在多個相互信任的系統中,用戶只須要登陸一次就能夠訪問其餘受信任的系統。java
新浪微博與新浪博客是相互信任的應用系統。mysql
*當用戶首次訪問新浪微博時,新浪微博識別到用戶未登陸,將請求重定向到認證中心,認證中心也識別到用戶未登陸,則將請求重定向到登陸頁。git
*當用戶已登陸新浪微博訪問新浪博客時,新浪博客識別到用戶未登陸,將請求重定向到認證中心,認證中心識別到用戶已登陸,返回用戶的身份,此時用戶無需登陸便可使用新浪博客。github
*只要多個系統使用同一套單點登陸框架那麼它們將是相互信任的。web
Yale 大學發起的一個開源項目,旨在爲 Web 應用系統提供一種可靠的單點登陸方法, CAS 在 2004 年 12 月正式成爲 JA-SIG 的一個項目。 spring
CAS包含CAS Client 和 CAS Server兩部分sql
CAS Client:要使用單點登陸的Web應用,將與同組下的Web應用構成相互信任的關係,只需在web應用中添加CAS提供的Listener和Filter便可成爲CAS Client ,其主要負責對客戶端的請求進行登陸校驗、重定向和校驗ticket工做。apache
CAS Server:主要負責對用戶的用戶名/密碼進行認證,頒發票據等,須要單獨的進行部署。json
*同組下的任意一個Web應用登陸後其餘應用都不須要登陸便可使用。瀏覽器
將下載的源碼包中的cas-server-webapp工程導入ide中,將工程打包爲war包,直接放入tomcat下的webapp中運行。
*CAS 5.0版本以上須要jdk1.8和gradle進行構建、4.X版本使用maven進行構建(maven 3.3+)
*因爲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/
登陸處理地址: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對象。
*當註銷成功後,此時再訪問登陸頁面時需從新登陸。
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&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
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>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.2.0</version>
</dependency>
<!-- 單點退出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()獲取當前登陸用戶的用戶名。
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_的屬性,所以容許訪問資源。
*因爲攜帶了SessionId Cookie而且能成功匹配Session對象,因爲已登陸過,Session中存在name爲_const_cas_assertion_的屬性,所以容許訪問資源。
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_的屬性,所以容許訪問資源。
訪問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>
*當關閉瀏覽器後Cookie失效(SessionId、TGC失效),此時再訪問項目A和項目B時將須要從新登陸。
*當CAS Server重啓後,TGT將會失效(位於服務器內存),TGC沒法成功匹配TGT,但此時訪問項目A和項目B時不須要從新登陸,由於其Session對象中仍存在Assertion實體。
*當CAS Client重啓後,無須再登陸也能夠使用。