cas客戶端流程詳解(源碼解析)–單點登錄
部落客之前一直使用了cas客戶端進行用戶的單點登錄操作,決定進行源碼分析來看cas的整個流程,以便以後出現了問題還不知道是什麼原因導致的
cas主要的形式就是通過過濾器的形式來實現的,來,貼上示例配置:


1 <listener> 2 <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class> 3 </listener> 4 5 <filter> 6 <filter-name>SSO Logout Filter</filter-name> 7 <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class> 8 </filter> 9 10 <filter-mapping> 11 <filter-name>SSO Logout Filter</filter-name> 12 <url-pattern>/*</url-pattern> 13 </filter-mapping> 14 15 <!-- SSO單點登錄認證filter --> 16 <filter> 17 <filter-name>SSO Authentication Filter</filter-name> 18 <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> 19 <init-param> 20 <!-- SSO伺服器地址 --> 21 <param-name>SSOServerUrl</param-name> 22 <param-value>//sso.jxeduyun.com/sso</param-value> 23 </init-param> 24 <init-param> 25 <!-- 統一登錄地址 --> 26 <param-name>SSOLoginUrl</param-name> 27 <param-value>//www.jxeduyun.com/App.ResourceCloud/Src/index.php</param-value> 28 </init-param> 29 <init-param> 30 <!-- 應用伺服器地址, 域名或者[//|//]{ip}:{port} --> 31 <param-name>serverName</param-name> 32 <param-value>//127.0.0.1:9000</param-value> 33 </init-param> 34 <init-param> 35 <!-- 除了openId,是否需要返回loginName以及userId等更多資訊 --> 36 <param-name>needAttribute</param-name> 37 <param-value>true</param-value> 38 </init-param> 39 <init-param> 40 <!-- 可選,不需要單點登錄的頁面,多個頁面以英文逗號分隔,支援正則表達式形式 --> 41 <!-- 例如:/abc/.*\.jsp,/.*/index\.jsp --> 42 <param-name>excludedURLs</param-name> 43 <param-value>/site2\.jsp</param-value> 44 </init-param> 45 </filter> 46 47 <filter-mapping> 48 <filter-name>SSO Authentication Filter</filter-name> 49 <url-pattern>/TyrzLogin/*</url-pattern> 50 </filter-mapping> 51 52 <!-- SSO ticket驗證filter --> 53 <filter> 54 <filter-name>SSO Ticket Validation Filter</filter-name> 55 <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class> 56 <init-param> 57 <!-- 應用伺服器地址, 域名或者[//|//]{ip}:{port} --> 58 <param-name>serverName</param-name> 59 <param-value>//127.0.0.1:9000</param-value> 60 </init-param> 61 <init-param> 62 <!-- 除了openId,是否需要返回loginName以及userId等更多資訊 --> 63 <param-name>needAttribute</param-name> 64 <param-value>true</param-value> 65 </init-param> 66 <init-param> 67 <!-- SSO伺服器地址前綴,用於生成驗證地址,和SSOServerUrl保持一致 --> 68 <param-name>SSOServerUrlPrefix</param-name> 69 <param-value>//sso.jxeduyun.com/sso</param-value> 70 </init-param> 71 </filter> 72 73 <filter-mapping> 74 <filter-name>SSO Ticket Validation Filter</filter-name> 75 <url-pattern>/*</url-pattern> 76 </filter-mapping>
web.xml
部落客用的不是官方的cas的jar包,是第三方要求的又再次封裝的jar包,不過就是屬性,獲取用戶資訊的邏輯多了點,其他的還是官方的源碼,部落客懶 的下載官方的jar在進行一步一步的debug看源碼了。
基本配置是添加4個過濾器,請求的時候可以進行攔截進行查看,最後一個是jfinal的開發框架,類似spring,不用管,
以上是jetty抓到請求時,進行獲取過濾的流程,只關注cas的這四個,裡面涉及到了快取過濾器(節點類型存儲)
全部進行路徑URL匹配完之後,會獲取到需要進行執行的過濾器,SSO Logout Filter->SSO Authentication Filter->SSO Ticket Validation Filter->CAS Assertion Thread Local Filter->jfinal->default
那我們就來一個一個看看,每個過濾器都做了哪些事。
SSO Logout Filter,從名字上看,應該是個退出的流程操作。來源嗎附上:
1 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 2 HttpServletRequest request = (HttpServletRequest)servletRequest; 3 HttpServletResponse response = (HttpServletResponse)servletResponse; 4 //查看請求中是否帶有ticket參數 5 if (!handler.isTokenRequest(request) && !CommonUtils.isNotBlank(request.getParameter("ticket"))) { 6 //如果沒有的ticket參數,查看是否是退出請求 7 if (handler.isLogoutRequest(request)) { 8 if (this.sessionMappingStorage != null && !this.sessionMappingStorage.getClass().equals(HashMapBackedSessionMappingStorage.class)) { 9 //是退出請求,直接銷毀session,直接return,不會在執行其他過濾器 10 handler.destroySession(request, response); 11 return; 12 } 13 this.log.trace("Ignoring URI " + request.getRequestURI()); 14 } else { 15 handler.recordSession(request); 16 } 17 ///繼續執行下一個執行器 18 filterChain.doFilter(servletRequest, servletResponse); 19 }
AuthenticationFilter,該過濾器主要做法:
1 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 2 String requestedUrl = ((HttpServletRequest)servletRequest).getServletPath(); 3 boolean isExcludedUrl = false; 4 //這裡會獲取到xml中的排除需要過濾的URL配置 5 if (this.excludedRequestUrlPatterns != null && this.excludedRequestUrlPatterns.length > 0) { 6 Pattern[] arr$ = this.excludedRequestUrlPatterns; 7 int len$ = arr$.length; 8 9 for(int i$ = 0; i$ < len$; ++i$) { 10 Pattern p = arr$[i$]; 11 if (isExcludedUrl = p.matcher(requestedUrl).matches()) { 12 break; 13 } 14 } 15 } 16 17 HttpServletRequest request = (HttpServletRequest)servletRequest; 18 HttpServletResponse response = (HttpServletResponse)servletResponse; 19 //如果當前URL是被排除,不需要校驗cas單點登錄的話,直接跳過當前過濾器,進行下一步 20 if (this.isIgnoreSSO() && isExcludedUrl) { 21 filterChain.doFilter(request, response); 22 } else { 23 //如果當前不被排除在外,查看白名單URL,也可以直接跳過該過濾器 24 boolean isWhiteUrl = false; 25 if (this.whiteRequestUrlPatterns != null && this.whiteRequestUrlPatterns.length > 0) { 26 Pattern[] arr$ = this.whiteRequestUrlPatterns; 27 int len$ = arr$.length; 28 29 for(int i$ = 0; i$ < len$; ++i$) { 30 Pattern p = arr$[i$]; 31 if (isWhiteUrl = p.matcher(requestedUrl).matches()) { 32 break; 33 } 34 } 35 } 36 37 if (isWhiteUrl) { 38 filterChain.doFilter(request, response); 39 } else { 40 //如果都沒匹配上,說明該URL是需要進行校驗查看的 41 HttpSession session = request.getSession(false); 42 //從session中取出改屬性值,查看當前session是否已經認證過了。如果認證過了了,可以跳過該過濾器 43 Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null; 44 //第一次請求的時候,改對象一定為null,因為沒人登錄過 45 if (assertion != null) { 46 filterChain.doFilter(request, response); 47 } else { 48 String serviceUrl = this.constructServiceUrl(request, response); 49 String ticket = CommonUtils.safeGetParameter(request, this.getArtifactParameterName()); 50 //查看是否session中有_const_cas_gateway_該屬性值,第一次登錄也沒有 51 boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); 52 //如果都沒有 53 if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) { 54 String encodedService; 55 //查看是否是cas伺服器return回調我們的這個介面請求,該屬性值在下面,也就是第一次登錄的時候,設置的 56 if (request.getSession().getAttribute("casreturn") != null) { 57 request.getSession().removeAttribute("casreturn"); 58 if (isExcludedUrl) { 59 filterChain.doFilter(request, response); 60 } else { 61 encodedService = Base64.encodeBase64String(serviceUrl.getBytes()); 62 encodedService = encodedService.replaceAll("[\\s*\t\n\r]", ""); 63 if (!this.SSOLoginUrl.startsWith("//") && !this.SSOLoginUrl.startsWith("//")) { 64 this.SSOLoginUrl = this.getServerName() + (this.getServerName().endsWith("/") ? "" : "/") + this.SSOLoginUrl; 65 } 66 //-------------@這裡---------------------- 67 //一直以為是所有校驗都沒有參數後,在下面才是跳轉到登錄頁,,沒想到,直接回調了,並沒有讓用戶去登陸,而是在這裡才去調用登錄頁 68 //讓用戶去登陸。大坑 69 response.sendRedirect(CommonUtils.joinUrl(this.SSOLoginUrl, "nextpage=" + encodedService)); 70 } 71 } else { 72 //第一次登錄的時候是這裡,他會將你xml中的cas伺服器地址拼接成login登錄地址,我們當前請求的URL編碼之後,會被cas登錄成功後回調使用 73 encodedService = this.SSOServerUrl + "/login?service=" + URLEncoder.encode(serviceUrl, "UTF-8") + "&redirect=true"; 74 //並且設置cas伺服器回調標識 75 request.getSession().setAttribute("casreturn", true); 76 //第一次登錄的時候,只能到這裡了,因為ticket參數,或則session中_const_cas_assertion_屬性都沒有,只能去cas伺服器請求登錄, 77 //這裡有個坑,,沒想到在這裡沒有直接出現登錄頁,而是調用cas伺服器地址後,直接返回來了,而且會在@那裡再去調用登錄地址 78 response.sendRedirect(encodedService); 79 //其他的事情後續就不要再debug了,已經跟我們cas沒有啥關係了,部落客,debug了半天越看越懵,才發現是服務在做其他的事情, 80 // 我們的登錄頁面早就已經出現了 81 } 82 } else { 83 filterChain.doFilter(request, response); 84 } 85 } 86 } 87 } 88 }
上面的還有一個坑,就是,在用戶登錄成功後,回調我們的地址,第一次並不會帶給我們ticket參數,而且還會走
ncodedService = this.SSOServerUrl + "/login?service=" + URLEncoder.encode(serviceUrl, "UTF-8") + "&redirect=true";
這個邏輯,並且附上casreturn屬性,然後,cas伺服器這回才會把ticket參數返回給我們的介面,剩下的就是下一個過濾器的事情了,慢慢來:
好了,這次有ticket了,我們來看下一個過濾器SSO Ticket Validation Filter
1 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 2 //這裡做了點事,是否為代理,部落客沒用這個,默認代理為null,返回true 3 if (this.preFilter(servletRequest, servletResponse, filterChain)) { 4 HttpServletRequest request = (HttpServletRequest)servletRequest; 5 HttpServletResponse response = (HttpServletResponse)servletResponse; 6 //獲取ticket請求參數 7 String ticket = CommonUtils.safeGetParameter(request, this.getArtifactParameterName()); 8 //到這裡了,分為三種情況, 9 //有ticket,因為你已經登錄了,cas伺服器登錄成功返回給你了,接下來進行校驗 10 //無ticket,可能你沒有配置第一個過濾器,溜進來了 11 //無ticket,ticket已經校驗成功後跳轉回來了,用戶屬性已經設置到session中了,所以這次請求沒有ticket了,不用去校驗 12 if (CommonUtils.isNotBlank(ticket)) { 13 if (this.log.isDebugEnabled()) { 14 this.log.debug("Attempting to validate ticket: " + ticket); 15 } 16 17 try { 18 //開始ticket票據校驗,這才是這個ticket過濾器真正要做的 19 //constructServiceUrl這個方法不用管,就是拼接一下URL路徑,把我的APPID啥的拼接上去 20 //validate做了挺多事,請看下一個類注釋,這裡先過去(大概邏輯就是去cas伺服器驗證ticket) 21 Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response)); 22 if (this.log.isDebugEnabled()) { 23 this.log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName()); 24 } 25 //看到這裡沒有,就是在第一個過濾器進行校驗的參數,如果ticket驗證成功,就會往request,及session設置屬性,該屬性就是_const_cas_assertion_ 26 //該屬性值則是一個用戶資訊map 27 request.setAttribute("_const_cas_assertion_", assertion); 28 if (this.useSession) { 29 request.getSession().setAttribute("_const_cas_assertion_", assertion); 30 } 31 //空方法,不用管 32 this.onSuccessfulValidation(request, response, assertion); 33 //ticket驗證成功後,在進行跳轉,這次是跳到我們自己的請求地址 34 if (this.redirectAfterValidation) { 35 this.log.debug("Redirecting after successful ticket validation."); 36 response.sendRedirect(this.constructServiceUrl(request, response)); 37 return; 38 } 39 } catch (TicketValidationException var8) { 40 response.setStatus(403); 41 this.log.warn(var8, var8); 42 this.onFailedValidation(request, response); 43 if (this.exceptionOnValidationFailure) { 44 throw new ServletException(var8); 45 } 46 47 return; 48 } 49 } 50 51 filterChain.doFilter(request, response); 52 } 53 }
裡面的ticket驗證邏輯在此:
1 public Assertion validate(String ticket, String service) throws TicketValidationException { 2 //此處是拼接好要調用的URL 3 ////sso.jxeduyun.com/sso/,該路徑是在web.xml中改ticket過濾器進行配置的SSOServerUrlPrefix 4 ////sso.jxeduyun.com/sso/serviceValidate?needAttribute=true&ticket=ST-28699-qdyblKpRwc5LpLk57dRM-sso.jxeduyun.com&service=http%3A%2F%2F127.0.0.1%3A9000%2Fdsideal_yy%2FdsTyrzLogin%2FssoLogin%3FloginType%3Dweb%26from%3Dew%26appId%3D00000&appKey=00000 5 String validationUrl = this.constructValidationUrl(ticket, service); 6 if (this.log.isDebugEnabled()) { 7 this.log.debug("Constructing validation url: " + validationUrl); 8 } 9 10 try { 11 this.log.debug("Retrieving response from server."); 12 //這裡不用看,就是發起請求調用上面的介面,查看ticket有效性 13 String serverResponse = this.retrieveResponseFromServer(new URL(validationUrl), ticket); 14 if (serverResponse == null) { 15 throw new TicketValidationException("The CAS server returned no response."); 16 } else { 17 if (this.log.isDebugEnabled()) { 18 this.log.debug("Server response: " + serverResponse); 19 } 20 //這個不用看了,就是解析返回的cas數據,然後獲取裡面的用戶資訊,並封裝成map 21 return this.parseResponseFromServer(serverResponse); 22 } 23 } catch (MalformedURLException var5) { 24 throw new TicketValidationException(var5); 25 } 26 }
因為ticket驗證成功後並沒有直接到下一個過濾器,而是從新請求了一次,這次不會有ticket參數了,因為session中已經有屬性了,就在前幾個過濾器中進行判斷,在都走一次,然後才會到下面這個過濾器
1 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 2 HttpServletRequest request = (HttpServletRequest)servletRequest; 3 HttpSession session = request.getSession(false); 4 Assertion assertion = (Assertion)((Assertion)(session == null ? request.getAttribute("_const_cas_assertion_") : session.getAttribute("_const_cas_assertion_"))); 5 6 try { 7 //該過濾器的作用就是,把用戶對象從session中拿出來,放到AssertionHolder裡面,從而在程式碼中獲取對象資訊的時候, 8 //直接調用該對象即可 9 AssertionHolder.setAssertion(assertion); 10 filterChain.doFilter(servletRequest, servletResponse); 11 } finally { 12 AssertionHolder.clear(); 13 } 14 15 }
至此,cas的登錄流程全部走完,不知道大家看懂多少,花了部落客大概一天的時間才把源碼理解通,ticket返回示例給大家一下,還有程式碼調用:
1 失敗示例: 2 <cas:serviceResponse xmlns:cas='//www.yale.edu/tp/cas'> 3 <cas:authenticationFailure code='INVALID_TICKET'> 4 ticket 'ST-28699-qdyblKpRwc5LpLk57dRM-sso.jxeduyun.com' not recognized 5 </cas:authenticationFailure> 6 </cas:serviceResponse> 7 成功示例: 8 <cas:serviceResponse xmlns:cas='//www.yale.edu/tp/cas'> 9 <cas:authenticationSuccess> 10 <cas:user>test</cas:user> 11 <cas:attributes> 12 <cas:multipleId>test-test-test-test-test</cas:multipleId> 13 14 <cas:userId>test</cas:userId> 15 16 <cas:loginName>test</cas:loginName> 17 18 </cas:attributes> 19 </cas:authenticationSuccess> 20 </cas:serviceResponse>
程式碼調用示例:
1 Assertion assertion = AssertionHolder.getAssertion(); 2 String openId = assertion.getPrincipal().getName(); 3 Map<String, Object> attributes = assertion.getPrincipal().getAttributes(); 4 String userId = attributes.get("userId").toString(); 5 String loginName = attributes.get("loginName").toString(); 6 System.out.println("openId:"+openId); 7 System.out.println("userId:"+userId); 8 System.out.println("loginName:"+loginName);