1. 程式人生 > >JAVA實現CAS單點登入總結

JAVA實現CAS單點登入總結

本文主要提供單點登入實現思路,以及實現過程中踩的坑。

一、專案需求:多個系統,通過一個平臺賬號實現單點登入。

二、實現思路:

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>

就說到這吧。