CAS實現單點登陸

1.簡介

 

SSO單點登陸

 

在多個相互信任的系統中,用戶只須要登陸一次就能夠訪問其餘受信任的系統。java

 

 

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

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

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

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

 

CAS 

 

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

 

 

CAS包含CAS ClientCAS Server兩部分sql

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

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

*同組下的任意一個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重啓後,無須再登陸也能夠使用。

相關文章
相關標籤/搜索