CAS實現單點登錄


1.簡介

 

SSO單點登錄

 

在多個相互信任的系統中,用戶只需要登錄一次就可以訪問其他受信任的系統。

 

 

新浪微博與新浪博客是相互信任的應用系統。

*當用戶首次訪問新浪微博時,新浪微博識別到用戶未登錄,將請求重定向到認證中心,認證中心也識別到用戶未登錄,則將請求重定向到登錄頁。

*當用戶已登錄新浪微博訪問新浪博客時,新浪博客識別到用戶未登錄,將請求重定向到認證中心,認證中心識別到用戶已登錄,返回用戶的身份,此時用戶無需登錄即可使用新浪博客。

*只要多個系統使用同一套單點登錄框架那么它們將是相互信任的。

 

CAS 

 

Yale 大學發起的一個開源項目,旨在為 Web 應用系統提供一種可靠的單點登錄方法, CAS 在 2004 年 12 月正式成為 JA-SIG 的一個項目。 

 

 

CAS包含CAS ClientCAS Server兩部分

CAS Client:要使用單點登錄的Web應用,將與同組下的Web應用構成相互信任的關系,只需在web應用中添加CAS提供的ListenerFilter即可成為CAS Client ,其主要負責對客戶端的請求進行登錄校驗、重定向和校驗ticket工作。

CAS Server:主要負責對用戶的用戶名/密碼進行認證,頒發票據等,需要單獨的進行部署。

*同組下的任意一個Web應用登錄后其他應用都不需要登錄即可使用。

 

 

2.CAS服務器搭建

 

2.1 去CAS官網下載CAS源碼包

 

將下載的源碼包中的cas-server-webapp工程導入ide中,將工程打包為war包,直接放入tomcat下的webapp中運行。

*CAS 5.0版本以上需要jdk1.8和gradle進行構建、4.X版本使用maven進行構建(maven 3.3+)

 

 

2.2 在Tomcat中開啟HTTPS協議

 

*由於CAS Server默認使用HTTPS協議進行訪問,因此需要在Tomcat中開啟HTTPS協議。

 

1.使用JDK提供的keytool命令生成秘鑰庫。

 

2.修改tomcat配置並開啟8443端口

 

在tomcat/conf/server.xml中添加:

<!-- 單向認證 -->
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreFile="www.gimc.cn.keystore" keystorePass="123456"  />

 

校驗Tomcat是否支持HTTPS協議: https://localhost:8443/

 

 

2.3 進入CAS認證中心

 

登錄處理地址:https://localhost:8443/cas-server-webapp-4.2.7/login

 

 

*由於首次訪問,客戶端瀏覽器進程所占用的內存中不存在TGC Cookie,所以CAS Server認為用戶未進行登錄,因此將請求轉發到登錄頁面。

*默認賬號:casuser/Mellon

 

 

 

*當登錄后再次訪問登錄處理時,將會直接轉發到已登錄頁面。

*CAS Server根據Cookie (TGC是否能夠匹配TGT)來判斷用戶是否已進行登錄,默認情況下TGC Cookie位於瀏覽器進程所占用的內存中,因此當關閉瀏覽器時Cookie失效(TGC失效),此時再訪問CAS登錄處理時將需要重新進行登錄,當CAS服務器重啟時,TGT將會失效(位於服務器內存),此時也需要重新進行登錄。

*當用戶登錄后,CAS Server會維護TGT與用戶身份信息的關系,所有CAS Client可以從CAS Server中獲取當前登錄的用戶的身份信息。 

 

注銷處理地址:https://localhost:8443/cas-server-webapp-4.2.7/logout

 

 

*在已登錄的狀態下訪問注銷地址將會提示注銷成功,其經過以下步驟:

1.清除保存在客戶端瀏覽器進程所占用的內存中的TGC Cookie(設空)

2.清除保存在服務器的TGT。

3.通過HTTP請求分別通知當前用戶所有已登錄的CAS Client進行注銷登錄操作,銷毀用戶對應的Session對象。

 

*當注銷成功后,此時再訪問登錄頁面時需重新登錄。

 

 

2.4 修改為自定義數據源

 

1.修改cas-server-webapp/WEB-INF/deployerConfigContext.xml

 

注釋配置:

<!-- <alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" /> -->

 

新增配置:

  <!-- 對密碼進行加密 -->
  <bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
    <constructor-arg value="MD5"></constructor-arg>
    <property name="characterEncoding" value="UTF-8"></property>
  </bean>

  <!-- 自定義數據源 -->
  <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/cas?useUnicode=true&amp;characterEncoding=UTF-8"></property>
    <property name="user" value="root"></property>
    <property name="password" value="root"></property>
  </bean>

  <!-- 認證控制器 -->
  <bean id="queryDatabaseAuthenticationHandler" name="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
    <property name="passwordEncoder" ref="passwordEncoder" />
    <property name="dataSource" ref="dataSource" />
    <!-- 通過用戶名查詢密碼的SQL --> 
    <property name="sql" value="select password from sys_user where username =?" />
  </bean>

 

2.在cas-server-webapp/WEB-INF/lib包中添加:cas-server-support-jdbc.jar、mysql-connector-java.jar

 

 

2.5 修改為HTTP方式訪問

 

1.修改cas-server-webapp/WEB-INF/cas.properties

tgc.secure=false warn.cookie.secure=false

 

2.修改cas-server-webapp/WEB-INF/classes/services/HTTPSandIMAPS-10000001.json

"serviceId" : "^(https|imaps|http)://.*"

*修改serviceId的值即可。

 

3.刪除cas-server-webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp頁面中校驗是否是HTTPS協議的標簽塊。

<c:if test="${not pageContext.request.secure}">
    <div id="msg" class="errors">
        <h2><spring:message code="screen.nonsecure.title" /></h2>
        <p><spring:message code="screen.nonsecure.message" /></p>
    </div>
</c:if>

 

 

3.CAS客戶端搭建

 

3.1 引入Maven依賴

 

<dependency>
      <groupId>org.jasig.cas.client</groupId>
      <artifactId>cas-client-core</artifactId>
      <version>3.2.0</version>
</dependency>

 

 

3.2 在web.xml中配置CAS提供的Listener、Filter

 

<!-- 單點退出Listener -->
<listener>
    <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>

<!-- 單點退出Filter -->
<filter>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- CAS認證Filter -->
<filter>
    <filter-name>CASFilter</filter-name>
    <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <!-- CAS登錄頁面,當SessionId無法匹配Session時,跳轉到CAS登錄頁面 -->
            <param-name>casServerLoginUrl</param-name>
            <param-value>http://localhost:8080/cas-server-webapp-4.2.7/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
</filter>
<filter-mapping>
    <filter-name>CASFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- CAS Ticket校驗Filter -->
<filter>
    <filter-name>CAS Validation Filter</filter-name>
    <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
        <init-param>
            <param-name>casServerUrlPrefix</param-name>
            <param-value>http://localhost:8080/cas-server-webapp-4.2.7</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
</filter>
<filter-mapping>
    <filter-name>CAS Validation Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 使客戶端支持通過AssertionHolder來獲取用戶的登錄名 -->  
<filter>  
    <filter-name>CAS Assertion Thread Local Filter</filter-name>       
    <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>  
</filter>  
<filter-mapping>  
    <filter-name>CAS Assertion Thread Local Filter</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>  

 

*各個客戶端可通過AssertionHolder.getAssertion().getPrincipal().getName()獲取當前登錄用戶的用戶名。

 

 

4.CAS原理分析

 

 

4.1 項目架構圖

 

 

4.2 用戶第一次訪問項目A

 

http://localhost:8080/A/testCas

 

1.請求將到達項目A的CAS認證Filter。

2.CAS認證Filter判斷是否能通過SessionId Cookie匹配到Session對象,並且Session對象中是否存在name為_const_cas_assertion_的屬性(該屬性中存放着Assertion實體)

3.若存在Assertion實體,則放行,將請求交給下一個過濾器進行處理( ticket檢驗filter ),若不存在Assertion實體,則構造Service參數,並且判斷請求中是否攜帶了ticket參數。

4.若存在ticket參數,則放行,將請求交給下一個過濾器進行處理( ticket檢驗filter ),若不存在ticket參數,則將請求重定向到CAS Server登錄處理。

 

CAS認證Filter

 

    public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        //當SessionId Cookie無法匹配Session時返回null,並不會創建新的Session對象.
        final HttpSession session = request.getSession(false);
        //判斷Session中是否存在name為_const_cas_assertion_的屬性,存在則返回Assertion實體,否則返回Null.
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
        
        //存在Assertion實體則直接放行,將請求交給下一個過濾器處理.
        if (assertion != null) {
            filterChain.doFilter(request, response);
            return;
        }

        //構造ServiceUrl用於封裝在service參數中.
        final String serviceUrl = constructServiceUrl(request, response);
        //判斷請求中是否存在ticket參數,若存在則說明是CAS Server的回調請求.
        final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName());
        final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
        //存在ticket參數則直接放行,將請求交給下一個過濾器處理,否則將請求重定向到CAS Server登錄處理,並在請求URL后追加service參數傳遞原訪問的URL.
        if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
            filterChain.doFilter(request, response);
            return;
        }

        final String modifiedServiceUrl;

        log.debug("no ticket and no assertion found");
        if (this.gateway) {
            log.debug("setting gateway attribute in session");
            modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
        } else {
            modifiedServiceUrl = serviceUrl;
        }

        if (log.isDebugEnabled()) {
            log.debug("Constructed service url: " + modifiedServiceUrl);
        }

        final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);

        if (log.isDebugEnabled()) {
            log.debug("redirecting to \"" + urlToRedirectTo + "\"");
        }

        response.sendRedirect(urlToRedirectTo);
    }

 

 

 

 

*由於用戶第一次訪問項目A,並沒有攜帶SessionId Cookie,因此無法成功匹配Session,所以Assertion實體為null,請求中也不存在ticket參數,則此時項目A認為該用戶未登錄,返回302狀態碼示意瀏覽器將請求重定向到CAS Server進行登錄處理,並在請求URL后追加service參數傳遞原訪問項目A的URL。

*CAS Client根據Session中是否存在Assertion屬性判斷當前用戶是否已登錄。

 

 

*當瀏覽器收到項目A返回的302重定向請求后,對重定向目標地址重新發起HTTP請求,最終到達CAS Server進行登錄處理,由於瀏覽器不存在TGC Cookie,CAS Server認為用戶未進行登錄,因此將請求轉發到登錄頁面。 

 

 

*輸入用戶名/密碼進行提交

 

 

*CAS Server對用戶輸入的用戶名/密碼進行校驗,若校驗成功則返回302狀態碼示意瀏覽器將請求重定向到原訪問項目A的URL地址並在URL后追加ticket參數傳遞ST,並且最終保存TGC Cookie在客戶端瀏覽器進程所占用的內存中。

TGC:Ticket Granted Cookie , 以Cookie的形式保存在客戶端瀏覽器所占用的內存中(Cookie值)

TGT:Ticket Granted Ticket,保存在CAS服務器的內存中,其可以簽發ST。

ST:Service Ticket,由TGT簽發,最終通過URL傳給CAS Client。

*CAS Server根據TGC匹配TGT,TGT又與用戶的身份信息相關聯。

*當用戶登錄成功后,此時客戶端就存在TGC Cookie,CAS服務端就存在對應的TGT。

 

 

 

*當瀏覽器收到CAS Server返回的302重定向請求后,對重定向目標地址重新發起HTTP請求( 攜帶ticket參數 ),此時請求將會首先進入項目A的CAS認證Filter,由於當前不存在SeesionId Cookie,不存在Session對象包含name為_const_cas_assertion_的屬性,但由於請求中包含了ticket參數,此時就會放行,將請求交給下一個過濾器處理。

 

5.請求將進入CAS Ticket驗證Filter。

6.判斷請求中是否存在ticket參數,若存在則進入Ticket校驗流程,否則直接放行,將請求交給下一個過濾器或直接到達目標資源。

7.若存在ticket,則通過HTTP的方式訪問CAS Server進行ticket的合法性校驗,若校驗成功則生成Session對象並且將Assertion實體放入Session中,最終將請求重定向原訪問項目的地址,若校驗失敗則返回403狀態碼,標識無權限訪問資源。

 

CAS Ticket校驗Filter

 

   public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {

        if (!preFilter(servletRequest, servletResponse, filterChain)) {
            return;
        }

        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
        //如果HttpServletRequest中包含ticket參數則進行ticket的合法性校驗,否則直接放行.
        if (CommonUtils.isNotBlank(ticket)) {
            if (log.isDebugEnabled()) {
                log.debug("Attempting to validate ticket: " + ticket);
            }

            try {
                //通過HTTP訪問CAS Server進行ticket的合法性校驗
                final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));

                if (log.isDebugEnabled()) {
                    log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());
                }

                request.setAttribute(CONST_CAS_ASSERTION, assertion);

                if (this.useSession) {
                    //當ticket校驗成功則將Assertion實體放入Session中
                    request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
                }
                onSuccessfulValidation(request, response, assertion);

                if (this.redirectAfterValidation) {
                    log. debug("Redirecting after successful ticket validation.");
                    //將請求重定向到原訪問的URL
                    response.sendRedirect(constructServiceUrl(request, response));
                    return;
                }
            } catch (final TicketValidationException e) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                log.warn(e, e);

                onFailedValidation(request, response);

                if (this.exceptionOnValidationFailure) {
                    throw new ServletException(e);
                }

                return;
            }
        }

        filterChain.doFilter(request, response);

    }

 

 

 

 

 

 

 

*當瀏覽器收到項目A返回的302重定向請求后,重新請求最初訪問項目A的URL地址。

*由於攜帶了SessionId Cookie並且能成功匹配Session對象,由於已登錄過,Session中存在name為_const_cas_assertion_的屬性,因此允許訪問資源。

 

 

4.3 用戶再次訪問項目A

 

 

*由於攜帶了SessionId Cookie並且能成功匹配Session對象,由於已登錄過,Session中存在name為_const_cas_assertion_的屬性,因此允許訪問資源。

 

 

4.4 用戶第一次訪問項目B

 

http://localhost:8080/B/testCas

 

 

*用戶第一次訪問項目B,並沒有攜帶SessionId Cookie,因此無法成功匹配Session,所以Assertion實體為null,請求中也不存在ticket參數,此時項目B認為該用戶未登錄,返回302狀態碼示意瀏覽器將請求重定向到CAS Server進行登錄處理,並在請求URL后追加service參數傳遞原訪問項目B的URL。

 

 

*當瀏覽器收到項目B返回的302重定向請求后,對重定向目標地址重新發起HTTP請求,最終到達CAS Server進行登錄處理,由於客戶端瀏覽器中存在TGC Cookie,並且CAS Server成功根據TGC匹配TGT,所以CAS Server認為該用戶已經進行登錄,最終通過TGT簽發ST,返回302狀態碼示意瀏覽器將請求重定向到原訪問項目B的URL,並在URL追加ticket參數傳遞ST。

 

 

*當瀏覽器收到CAS Server返回的302重定向請求后,對重定向目標地址重新發起HTTP請求( 攜帶ticket參數 ),此時請求將會進入項目B的ticket認證Filter中,項目B將對ticket進行有效性校驗( 內部訪問Cas Server進行校驗 ),若校驗成功則生成Session對象並將Assertion實體放入Session中,最終將請求重定向到原訪問項目B的地址。

 

 

 

*當瀏覽器收到項目B返回的302重定向請求后,重新請求最初訪問項目B的URL地址。

*由於攜帶了SessionId Cookie並且能成功匹配Session對象,由於已登錄過,Session中存在name為_const_cas_assertion_的屬性,因此允許訪問資源。

 

 

4.5 注銷

 

訪問CAS Server注銷處理地址:http://localhost:8080/cas-server-webapp-4.2.7/logout

 

 

*當訪問CAS注銷地址后:

1.清除位於客戶端瀏覽器進程所占用的內存中的TGC Cookie (設空)

2.清除位於CAS Server中對應的TGT。

3.通過HTTP請求分別通知當前用戶所有已登錄的CAS Client進行注銷登錄操作,此時請求將會進入CAS Client的單點登出Filter,單點登出Filter中判斷當前請求是否是POST請求方式並且是否攜帶了logoutRequest參數,若不屬於則放行,將請求交給下一個過濾器進行處理,若屬於則進行Session對象的銷毀。

 

*當注銷后,TGC、TGT、CAS Client用戶對應的Session對象將會失效,此時再訪問項目A和項目B需要重新登錄。

 

CAS單點登出Filter

 

    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        //判斷請求參數是否攜帶ticket參數,即CAS Server的回調URL,用於Session的記錄操作.
        if (handler.isTokenRequest(request)) {
            handler.recordSession(request);
        //判斷請求參數是否攜帶logoutRequest參數,即CAS Server注銷時通知CAS Client的URL,用於Session的銷毀.
        } else if (handler.isLogoutRequest(request)) {
            handler.destroySession(request);
            // Do not continue up filter chain
            return;
        } else {
            log.trace("Ignoring URI " + request.getRequestURI());
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

 

 

 

 

 

    public void destroySession(final HttpServletRequest request) {
        //獲取HTTP請求中的logoutRequest參數
        final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName);
        if (log.isTraceEnabled()) {
            log.trace ("Logout request:\n" + logoutMessage);
        }
        //解析XML獲取ticket值
        final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
        //如果ticket值不為空則執行Session的invalidate()方法銷毀Session對象.
        if (CommonUtils.isNotBlank(token)) {
            final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);

            if (session != null) {
                String sessionID = session.getId();

                if (log.isDebugEnabled()) {
                    log.debug ("Invalidating session [" + sessionID + "] for token [" + token + "]");
                }
                try {
                    session.invalidate();
                } catch (final IllegalStateException e) {
                    log.debug("Error invalidating session.", e);
                }
            }
        }
    }

 

 

 

 

*logoutRequest參數的值是XML:

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-1-1zjgcguvShbJrsNLbbfQ5Rk5LbfHblgGHep" Version="2.0" IssueInstant="2018-07-23T16:46:32Z">
  <saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID>
  <samlp:SessionIndex>ST-1-2EiBwiJuD5vbhYghmMS5-cas01.example.org</samlp:SessionIndex>
</samlp:LogoutRequest>

 

 

4.6 關閉瀏覽器

 

*當關閉瀏覽器后Cookie失效(SessionId、TGC失效),此時再訪問項目A和項目B時將需要重新登錄。

 

 

4.7 重啟CAS Server

 

*當CAS Server重啟后,TGT將會失效(位於服務器內存),TGC無法成功匹配TGT,但此時訪問項目A和項目B時不需要重新登錄,因為其Session對象中仍存在Assertion實體。

*當CAS Client重啟后,無須再登錄也可以使用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM