前期準備: 1. cas-client-core-3.2.1.jar 2. cas-client-3.2.1-release.zip
3.應用系統webapp(http://127.0.0.1:8090/webapp/main.do)
4.CAS單點登入伺服器端(http://127.0.0.1:8081/cas-server/)
本次討論包括CAS單點登入用戶端的部分源碼,以及在此基礎上進行二次開發,因此需要修改部分CAS用戶端的源碼,源碼部分的修改在下面進行討論。關於 CAS伺服器端的源碼分析,請參考另一篇文章 http://blog.csdn.net/dovejing/article/details/44523545 其中cas-client-core-3.2.1.jar為CAS用戶端的核心jar包,cas-client-3.2.1-release.zip為CAS用戶端的源碼zip包。
web.xml部分代碼
<filter><filter-name>CASFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter><filter-name>CASValidationFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter><filter-name>initLoginUserFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter-mapping><filter-name>CASFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><filter-mapping><filter-name>CASValidationFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><filter-mapping><filter-name>initLoginUserFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
spring-context.xml部分代碼
<bean id="CASFilter" class="com.master.client.filter.AuthenticationFilter" init-method="initConfig"/><bean id="CASValidationFilter" class="com.master.client.filter.TicketValidationFilter" init-method="initConfig"/><bean id="initLoginUserFilter" class="com.master.client.filter.InitLoginUserFilter" init-method="initConfig"/>
cas_config.properties代碼
service = http://127.0.0.1:8090/webapp/main.docasServerUrlPrefix = http://127.0.0.1:8081/cas-server/casServerLoginUrl = http://127.0.0.1:8081/cas-server/logincasServerLogoutUrl = http://127.0.0.1:8081/cas-server/logouterrorUrl = http://127.0.0.1:8081/cas-server/error
首先,從web.xml配置資訊可以看出,有三個過濾器負責處理單點登入流程,其中前兩個為流程必須處理的過濾器,最後一個是我新增加的過濾器,主要功能是初始化使用者資訊和許可權。既然是源碼解讀,我們必然要看源碼。下面就從CAS用戶端的源碼給大家進行解讀CAS單點登入用戶端的工作流程。之後會單獨寫一篇關於CAS單點登入伺服器的源碼解讀的文章。
AuthenticationFilter
從單點登入第一個過濾器開始解讀,源碼中的第一個過濾器是AuthenticationFilter,因為業務需求對此過濾器進行了二次開發。源碼是從web.xml設定檔中讀取初始化參數,我對此做了修改,從properties檔案中讀取初始化參數。把單點登入的配置參數定義到properties檔案中,這樣做的好處是對初始化配置參數統一管理,一目瞭然。源碼走起。
AuthenticationFilter的父類AbstractCasFilter的初始化方法,主要工作是從properties中擷取單點登入的系統參數。使大家看的更清楚,源碼中省略了日誌的輸出。
public void initConfig() throws ServletException {Properties conf = PropertiesUtil.getConfigProperties();setServerName(conf.getProperty("serverName", null));setService(conf.getProperty("service", null));setArtifactParameterName(conf.getProperty("artifactParameterName", "ticket"));setServiceParameterName(conf.getProperty("serviceParameterName", "service"));setEncodeServiceUrl(parseBoolean(conf.getProperty("encodeServiceUrl", "true")));//登入錯誤跳轉到該url地址setErrorUrl(conf.getProperty("errorUrl", null));//初始化設定檔(AuthenticationFilter類)initInternalConfig(conf);init();//初始化}
AuthenticationFilter的初始化方法。主要工作是從properties中擷取單點登入的系統參數。使大家看的更清楚,源碼中省略了日誌的輸出。
public void initInternalConfig(Properties conf) throws ServletException {if (!isIgnoreInitConfiguration()) {setCasServerLoginUrl(conf.getProperty("casServerLoginUrl", null));setRenew(parseBoolean(conf.getProperty("renew", "false")));setGateway(parseBoolean(conf.getProperty("gateway", "false")));String gatewayStorageClass = conf.getProperty("gatewayStorageClass", null);if (gatewayStorageClass == null)return;try {this.gatewayStorage = ((GatewayResolver) Class.forName(gatewayStorageClass).newInstance());} catch (Exception e) {this.log.error(e, e);throw new ServletException(e);}}}
AuthenticationFilter的過濾方法。
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;final HttpSession session = request.getSession(false);//從session中擷取名為"_const_cas_assertion_"的Assertionfinal Assertion assertion = (session != null) ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;//如果存在,則說明已經登入,本過濾器處理完成,處理下個過濾器if (assertion != null) {filterChain.doFilter(request, response);return;}//產生serviceUrlfinal String serviceUrl = constructServiceUrl(request, response);//從request中擷取參數ticket(ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);//如果ticket不為空白,本過濾器處理完成,處理下個過濾器if ((CommonUtils.isNotBlank(ticket)) || (wasGatewayed)) {filterChain.doFilter(request, response);return;}this.log.debug("no ticket and no assertion found");final String modifiedServiceUrl;if (this.gateway) {this.log.debug("setting gateway attribute in session");modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);} else {modifiedServiceUrl = serviceUrl;}if (this.log.isDebugEnabled()) {this.log.debug("Constructed service url: " + modifiedServiceUrl);}//產生重新導向URLfinal String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(),modifiedServiceUrl, this.renew, this.gateway);if (this.log.isDebugEnabled()) {this.log.debug("redirecting to \"" + urlToRedirectTo + "\"");}//跳轉到CAS伺服器的登入頁面response.sendRedirect(urlToRedirectTo);}
當我們從瀏覽器訪問配置了單點登入的應用系統時(http://127.0.0.1:8090/webapp/main.do),由於整合了CAS單點登入用戶端,此時進入到第一個過濾器AuthenticationFilter(不考慮其他非單點登入的過濾器),執行以下操作:
從session中擷取名為“_const_cas_assertion_”的assertion對象,判斷assertion是否存在,如果存在,說明已經登入,執行下一個過濾器。如果不存在,執行第2步。 產生serviceUrl(http://127.0.0.1:8090/webapp/main.do),從request中擷取票據參數ticket,判斷ticket是否為空白,如果不為空白執行下一個過濾器。如果為空白,執行第3步。 產生重新導向URL(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp/main.do)。 跳轉到單點登入伺服器,顯示登入頁面,此時第一個過濾器執行完成。
TicketValidationFilter
AbstractTicketValidationFilter的過濾方法。
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;//從request中擷取參數final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());//ticket不為空白,驗證ticket,否則本過濾器處理完成,處理下個過濾器if (CommonUtils.isNotBlank(ticket)) {if (this.log.isDebugEnabled()) {this.log.debug("Attempting to validate ticket: " + ticket);}try {//驗證ticket併產生Assertion對象,錯誤拋出TicketValidationException異常final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));if (this.log.isDebugEnabled()) {this.log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());}//request設定assertionrequest.setAttribute(CONST_CAS_ASSERTION, assertion);//session設定assertionif (this.useSession) {request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);}onSuccessfulValidation(request, response, assertion);if (this.redirectAfterValidation) {this.log.debug("Redirecting after successful ticket validation.");response.sendRedirect(constructServiceUrl(request, response));return;}} catch (final TicketValidationException e) {response.setStatus(403);this.log.info("獲得使用者認證資訊失敗:" + e.getMessage());//跳轉到錯誤頁面response.sendRedirect(getErrorUrl() + "?errortype=" + e.getMessage() + "&" + createService());return;}}//本過濾器處理完成,處理下個過濾器filterChain.doFilter(request, response);}
假設當執行完第一個過濾器後,跳轉到CAS伺服器端的登入頁面,輸入使用者名稱和密碼,驗證通過後。CAS伺服器端會產生ticket,並將ticket作為重新跳轉到應用系統的參數(http://127.0.0.1:8090/webapp/main.do?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。此時又進入第一個過濾器AuthenticationFilter,由於存在ticket參數,進入到第二個過濾器TicketValidationFilter,執行以下操作: 從request擷取ticket參數,如果ticket為空白,繼續處理下一個過濾器。如果參數不為空白,驗證ticket參數的合法性。 驗證ticket,TicketValidationFilter的validate方法通過httpClient訪問CAS伺服器端(http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://127.0.0.1:8090/webapp/main.do)驗證ticket是否正確,並返回assertion對象。如果驗證失敗,拋出異常,跳轉到錯誤頁面。如果驗證成功,session會以"_const_cas_assertion_"的名稱儲存assertion對象,繼續處理下一個過濾器。 InitLoginUserFilter InitLoginUserFilter的過濾方法。
public final void doFilter(final ServletRequest servletRequest,final ServletResponse servletResponse, final FilterChain filterChain)throws IOException, ServletException {try {//加上自己應用的邏輯,例如構造使用者的資訊和許可權等//...//本過濾器處理完成,處理下個過濾器filterChain.doFilter(request, response);return;} catch (Exception e) {//跳轉到錯誤頁面res.sendRedirect(getErrorUrl() + "?errortype=" + e.getMessage() + "&" + createService());}}
這個過濾器是我新增加的過濾器,以滿足應用系統的特殊業務。當到達這個過濾器的時候,說明CAS伺服器已經登入成功,此過濾器會對應用系統所需要的資訊進行初始化(使用者基本資料和許可權等)。完成操作後,顯示應用系統的首頁。如果出現異常,跳轉到錯誤頁面。