JAVA實現CAS單點登入總結
阿新 • • 發佈:2019-01-23
本文主要提供單點登入實現思路,以及實現過程中踩的坑。
一、專案需求:多個系統,通過一個平臺賬號實現單點登入。
二、實現思路:
1、因多系統之間賬號並不相同,需要一個管理繫結賬號的平臺系統,以下統稱cas-admin系統。
比如A系統admin000賬號,B系統admin001賬號,都需要通過system賬號實現單點登入,這時候就需要system繫結這 兩個賬號。
2、採用CAS的開源系統,實現單點登入服務。本次使用版本是5.2.6
3、子系統通過SpringSecurity和CAS進行整合,實現單點登入。
二、具體實現:
本次主要說明一些CAS服務端的配置:
1、application.properties配置檔案,其中注意事項都寫在註釋裡面了。坑也寫在裡面了。
## # CAS Server Context Configuration # server.context-path=/cas server.port=8443 # server.ssl.key-store=file:/etc/cas/thekeystore # server.ssl.key-store-password=changeit # server.ssl.key-password=changeit # server.ssl.ciphers= # server.ssl.client-auth= # server.ssl.enabled= # server.ssl.key-alias= # server.ssl.key-store-provider= # server.ssl.key-store-type= # server.ssl.protocol= # server.ssl.trust-store= # server.ssl.trust-store-password= # server.ssl.trust-store-provider= # server.ssl.trust-store-type= # server.max-http-header-size=2097152 # server.use-forward-headers=true # server.connection-timeout=20000 # server.error.include-stacktrace=ALWAYS # server.compression.enabled=true # server.compression.mime-types=application/javascript,application/json,application/xml,text/html,text/xml,text/plain # server.tomcat.max-http-post-size=2097152 # server.tomcat.basedir=build/tomcat # server.tomcat.accesslog.enabled=true # server.tomcat.accesslog.pattern=%t %a "%r" %s (%D ms) # server.tomcat.accesslog.suffix=.log # server.tomcat.max-threads=10 # server.tomcat.port-header=X-Forwarded-Port # server.tomcat.protocol-header=X-Forwarded-Proto # server.tomcat.protocol-header-https-value=https # server.tomcat.remote-ip-header=X-FORWARDED-FOR # server.tomcat.uri-encoding=UTF-8 spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true spring.http.encoding.force=true ## # CAS Cloud Bus Configuration # spring.cloud.bus.enabled=false # spring.cloud.bus.refresh.enabled=true # spring.cloud.bus.env.enabled=true # spring.cloud.bus.destination=CasCloudBus # spring.cloud.bus.ack.enabled=true endpoints.enabled=false endpoints.sensitive=true endpoints.restart.enabled=false endpoints.shutdown.enabled=false management.security.enabled=true management.security.roles=ACTUATOR,ADMIN management.security.sessions=if_required management.context-path=/status management.add-application-context-header=false security.basic.authorize-mode=role security.basic.enabled=false security.basic.path=/cas/status/** ## # CAS Web Application Session Configuration # server.session.timeout=300 server.session.cookie.http-only=true server.session.tracking-modes=COOKIE ## # CAS Thymeleaf View Configuration # spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.cache=true spring.thymeleaf.mode=HTML ## # CAS Log4j Configuration # # logging.config=file:/etc/cas/log4j2.xml server.context-parameters.isLog4jAutoInitializationDisabled=true ## # CAS AspectJ Configuration # spring.aop.auto=true spring.aop.proxy-target-class=true ## # CAS Authentication Credentials # #cas.authn.accept.users=casuser::Mellon #此處是配置CAS登入使用者的,需要一個cas資料庫,自己配置一下 cas.authn.jdbc.query[0].sql=SELECT * FROM t_subuser WHERE username=? cas.authn.jdbc.query[0].healthQuery= cas.authn.jdbc.query[0].isolateInternalQueries=false cas.authn.jdbc.query[0].url=jdbc:mysql://10.10.0.102:3306/goldencis_cas?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false cas.authn.jdbc.query[0].failFast=true cas.authn.jdbc.query[0].isolationLevelName=ISOLATION_READ_COMMITTED cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect cas.authn.jdbc.query[0].leakThreshold=10 cas.authn.jdbc.query[0].propagationBehaviorName=PROPAGATION_REQUIRED cas.authn.jdbc.query[0].batchSize=1 cas.authn.jdbc.query[0].user=root cas.authn.jdbc.query[0].password=goldencis #cas.authn.jdbc.query[0].ddlAuto=create-drop cas.authn.jdbc.query[0].maxAgeDays=180 cas.authn.jdbc.query[0].autocommit=false cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver cas.authn.jdbc.query[0].idleTimeout=5000 # cas.authn.jdbc.query[0].credentialCriteria= # cas.authn.jdbc.query[0].name= # cas.authn.jdbc.query[0].order=0 # cas.authn.jdbc.query[0].dataSourceName= # cas.authn.jdbc.query[0].dataSourceProxy=false cas.authn.jdbc.query[0].fieldPassword=password cas.authn.jdbc.query[0].passwordEncoder.type=cn.goldencis.CustomPasswordEncoder cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8 #cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5 #cas.authn.jdbc.query[0].passwordEncoder.secret= #cas.authn.jdbc.query[0].passwordEncoder.strength=16 #CAS登入使用者多屬性返回,主要返回的是賬號繫結資訊(上文中說明的system繫結A、B子系統賬號資訊) cas.authn.attributeRepository.jdbc[0].attributes.extprop=extprop cas.authn.attributeRepository.jdbc[0].singleRow=true cas.authn.attributeRepository.jdbc[0].order=0 cas.authn.attributeRepository.jdbc[0].requireAllAttributes=true cas.authn.attributeRepository.jdbc[0].url=jdbc:mysql://10.10.0.102:3306/goldencis_cas?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false cas.authn.attributeRepository.jdbc[0].username=username cas.authn.attributeRepository.jdbc[0].user=root cas.authn.attributeRepository.jdbc[0].password=goldencis cas.authn.attributeRepository.jdbc[0].sql=select * from t_subuser where {0} cas.authn.attributeRepository.jdbc[0].dialect=org.hibernate.dialect.MySQLDialect cas.authn.attributeRepository.jdbc[0].ddlAuto=none cas.authn.attributeRepository.jdbc[0].driverClass=com.mysql.jdbc.Driver cas.authn.attributeRepository.jdbc[0].leakThreshold=10 cas.authn.attributeRepository.jdbc[0].propagationBehaviorName=PROPAGATION_REQUIRED cas.authn.attributeRepository.jdbc[0].batchSize=1 cas.authn.attributeRepository.jdbc[0].healthQuery=SELECT 1 cas.authn.attributeRepository.jdbc[0].failFast=true #此處是設定快取時間,當時做的時候CAS資料庫變化後,不生效,就是這個導致的,加上就好了。這個是個坑 cas.authn.attributeRepository.expireInMinutes=1 cas.authn.attributeRepository.maximumCacheSize=10000 cas.authn.attributeRepository.merger=REPLACE|ADD|MERGE # 配置http訪問 cas.tgc.secure=false cas.warningCookie.secure=false cas.serviceRegistry.initFromJson=true # logout重定向 cas.logout.followServiceRedirects=true # Default Expiration Policy tgt.maxTimeToLiveInSeconds=28800 #設定tgt過期時間 tgt.timeToKillInSeconds=3600
2、重點來了spring security整合CAS。
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.1.xsd"> <!-- 配置不過濾的資源(靜態資源) --> <http pattern="/GoldVSK/*.*" security="none"></http> <http pattern="/*.cab" security="none"></http> <http pattern="/js/*.js" security="none"></http> <http pattern="/js/*/*.js" security="none"></http> <http pattern="/js/*/*/*.js" security="none"></http> <http pattern="/js/*/*/*/*.js" security="none"></http> <http pattern="/js/**" security="none"></http> <http pattern="/skin/*/css/*.css" security="none"></http> <http pattern="/skin/*/css/*/*.css" security="none"></http> <http pattern="/skin/*/css/*/*/*.css" security="none"></http> <http pattern="/skin/*/images/*.*" security="none"></http> <http pattern="/skin/*/images/*/*.*" security="none"></http> <http pattern="/skin/images/*.*" security="none"></http> <http pattern="/loginCheck" security="none"></http> <http pattern="/data/**" security="none"></http> <http pattern="/subSystem/queryPrivateSubSystemList" security="none"></http> <http pattern="/logout" create-session="never" auto-config="true" > <anonymous enabled="false" /> <intercept-url pattern="/logout" method="POST" /> <csrf disabled="true" /> </http> <beans:bean id="accessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl"> <beans:property name="errorPage" value="/WEB-INF/jsp/common/403.jsp"/> </beans:bean> <http auto-config="true" entry-point-ref="casEntryPoint"> <headers defaults-disabled="true"> <cache-control/> </headers> <access-denied-handler ref="accessDeniedHandler" /> <intercept-url pattern="/**" access="hasRole('ROLE_USER') " /> <form-login login-page="/login" authentication-success-handler-ref="authenticationSuccessHandler" authentication-failure-url="/login?error=ture" username-parameter="userName" password-parameter="password" /> <custom-filter position="CAS_FILTER" ref="casFilter" /> <custom-filter before="LOGOUT_FILTER" ref="requestSingleLogoutFilter"/> <custom-filter before="CAS_FILTER" ref="singleLogoutFilter"/> </http> <!-- security cas begin --> <beans:bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter"> <beans:property name="authenticationManager" ref="authenticationManager"/> <beans:property name="authenticationSuccessHandler" ref="authenticationSuccessHandler"/> </beans:bean> <!-- This filter handles a Single Logout Request from the CAS Server --> <beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/> <!-- This filter redirects to the CAS Server to signal Single Logout should be performed --> <beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter"> <beans:constructor-arg value="${cas.server.url}/logout?service=${cas.client.url}"/> <beans:constructor-arg> <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/> </beans:constructor-arg> <beans:property name="filterProcessesUrl" value="/logout/cas"/> </beans:bean> <!-- 認證的入口 --> <beans:bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"> <!-- Cas Server的登入地址 --> <beans:property name="loginUrl" value="${cas.server.url}/login"/> <!-- service相關的屬性 --> <beans:property name="serviceProperties" ref="serviceProperties"/> </beans:bean> <!-- 指定service相關資訊 --> <beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties"> <!-- Cas Server認證成功後的跳轉地址,這裡要跳轉到我們的Spring Security應用,之後會由CasAuthenticationFilter處理 --> <beans:property name="service" value="${cas.client.url}/login/cas"/> <beans:property name="sendRenew" value="false"/> </beans:bean> <beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> <beans:property name="authenticationUserDetailsService" ref="userDetailService4Cas"> </beans:property> <beans:property name="serviceProperties" ref="serviceProperties" /> <!-- 配置TicketValidator在登入認證成功後驗證ticket --> <beans:property name="ticketValidator"> <beans:bean class="org.jasig.cas.client.validation.Cas30ProxyTicketValidator"> <!-- Cas Server訪問地址的字首,即根路徑--> <beans:constructor-arg index="0" value="${cas.server.url}" /> </beans:bean> </beans:property> <beans:property name="key" value="key4CasAuthenticationProvider"/> </beans:bean> <!-- security cas end --> <beans:bean id="userDetailService" class="cn.goldencis.cas.system.override.UserDetailServiceImpl" /> <!-- cas user service --> <beans:bean id="userDetailService4Cas" class="cn.goldencis.cas.system.override.UserDetailServiceImpl4Cas" /> <beans:bean id="authenticationSuccessHandler" class="cn.goldencis.cas.system.override.AuthenticationSuccessHandlerImpl" /> <beans:bean id="passwordEncoder" class="cn.goldencis.cas.system.override.PasswordEncoderImpl" /> <beans:bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <beans:property name="hideUserNotFoundExceptions" value="false" /> <beans:property name="userDetailsService" ref="userDetailService" /> <beans:property name="passwordEncoder" ref="passwordEncoder" /> </beans:bean> <!-- 自定義許可權管理,使用自己的user-service --> <authentication-manager alias="authenticationManager"> <authentication-provider ref="casAuthenticationProvider" /> <authentication-provider ref="authenticationProvider" /> </authentication-manager> <!-- 針對登入頁面,不用做CSRF了 --> <!-- <beans:bean id="csrfSecurityRequestMatcher" class="cn.goldencis.vsk.system.override.CsrfSecurityRequestMatcher" />--> </beans:beans>
3、CAS整合需要類,其中獲取CAS服務端傳遞過來的資訊是重點。
package cn.goldencis.tsa.system.override;
import cn.goldencis.tsa.system.entity.UserDO;
import cn.goldencis.tsa.system.entity.UserDOCriteria;
import cn.goldencis.tsa.system.service.IUserService;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
/**
* 實現UserDetailsService
*
* @author 2016年7月14日 上午10:54:51
*/
@SuppressWarnings("rawtypes")
public class UserDetailServiceImpl4Cas implements AuthenticationUserDetailsService {
private Logger log = LoggerFactory.getLogger(UserDetailsService.class);
@Resource
private IUserService userService;
private static final String EXTPROP = "extprop";
private static final String KEY = "TSA";
@Override
public UserDetails loadUserDetails(Authentication authentication) throws UsernameNotFoundException {
CasAssertionAuthenticationToken token = (CasAssertionAuthenticationToken) authentication;
String username = token.getName();
Map<String,Object> attributes = token.getAssertion().getPrincipal().getAttributes();
if(attributes != null) {
JSONObject jsonObject = JSONObject.parseObject(attributes.get(EXTPROP).toString());
username = jsonObject.getString(KEY);
}else {
throw new UsernameNotFoundException("登入異常");
}
getRequest().getSession().setAttribute("isLoginAction", 1);
UserDOCriteria userexample = new UserDOCriteria();
userexample.createCriteria().andUserNameEqualTo(username);
UserDO user = userService.getBy(userexample);
log.debug("loadUserByUsername user: " + user);
if ("system".equals(username)) {
getRequest().setAttribute("username", username.toUpperCase());
} else {
getRequest().setAttribute("username", username);
}
if (user == null) {
throw new UsernameNotFoundException("使用者名稱不正確");
}
Collection<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
if (user.getStatus() == 0) {
throw new UsernameNotFoundException("該使用者已暫時停用");
}
getRequest().getSession().setAttribute("isCasLogin", 1);
GrantedAuthority sim = new SimpleGrantedAuthority("ROLE_USER_" + user.getId());
GrantedAuthority simAnonymous = new SimpleGrantedAuthority("ROLE_ANONYMOUS");
auths.add(sim);
auths.add(simAnonymous);
User ruser = new User(username, user.getPassword(), auths);
return ruser;
}
/**
* 獲取request
*
* @return
*/
private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
if (sra != null) {
HttpServletRequest request = sra.getRequest();
return request;
}
throw new RuntimeException("無法獲取request物件。");
}
}
4、子系統pom依賴資訊
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>${spring.security.version}</version>
</dependency>
就說到這吧。