1. 程式人生 > 實用技巧 >cas客戶端流程詳解(原始碼解析)--單點登入

cas客戶端流程詳解(原始碼解析)--單點登入

博主之前一直使用了cas客戶端進行使用者的單點登入操作,決定進行原始碼分析來看cas的整個流程,以便以後出現了問題還不知道是什麼原因導致的

cas主要的形式就是通過過濾器的形式來實現的,來,貼上示例配置:

     <listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener> <filter>
<filter-name>SSO Logout Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter> <filter-mapping>
<filter-name>SSO Logout Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping> <!-- SSO單點登入認證filter -->
<filter>
<filter-name>SSO Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<!-- SSO伺服器地址 -->
<param-name>SSOServerUrl</param-name>
<param-value>http://sso.jxeduyun.com/sso</param-value>
</init-param>
<init-param>
<!-- 統一登入地址 -->
<param-name>SSOLoginUrl</param-name>
<param-value>http://www.jxeduyun.com/App.ResourceCloud/Src/index.php</param-value>
</init-param>
<init-param>
<!-- 應用伺服器地址, 域名或者[http://|https://]{ip}:{port} -->
<param-name>serverName</param-name>
<param-value>http://127.0.0.1:9000</param-value>
</init-param>
<init-param>
<!-- 除了openId,是否需要返回loginName以及userId等更多資訊 -->
<param-name>needAttribute</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<!-- 可選,不需要單點登入的頁面,多個頁面以英文逗號分隔,支援正則表示式形式 -->
<!-- 例如:/abc/.*\.jsp,/.*/index\.jsp -->
<param-name>excludedURLs</param-name>
<param-value>/site2\.jsp</param-value>
</init-param>
</filter> <filter-mapping>
<filter-name>SSO Authentication Filter</filter-name>
<url-pattern>/TyrzLogin/*</url-pattern>
</filter-mapping> <!-- SSO ticket驗證filter -->
<filter>
<filter-name>SSO Ticket Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<!-- 應用伺服器地址, 域名或者[http://|https://]{ip}:{port} -->
<param-name>serverName</param-name>
<param-value>http://127.0.0.1:9000</param-value>
</init-param>
<init-param>
<!-- 除了openId,是否需要返回loginName以及userId等更多資訊 -->
<param-name>needAttribute</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<!-- SSO伺服器地址字首,用於生成驗證地址,和SSOServerUrl保持一致 -->
<param-name>SSOServerUrlPrefix</param-name>
<param-value>http://sso.jxeduyun.com/sso</param-value>
</init-param>
</filter> <filter-mapping>
<filter-name>SSO Ticket Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</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,從名字上看,應該是個退出的流程操作。來源嗎附上:

 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//檢視請求中是否帶有ticket引數
if (!handler.isTokenRequest(request) && !CommonUtils.isNotBlank(request.getParameter("ticket"))) {
//如果沒有的ticket引數,檢視是否是退出請求
if (handler.isLogoutRequest(request)) {
if (this.sessionMappingStorage != null && !this.sessionMappingStorage.getClass().equals(HashMapBackedSessionMappingStorage.class)) {
//是退出請求,直接銷燬session,直接return,不會在執行其他過濾器
handler.destroySession(request, response);
return;
}
this.log.trace("Ignoring URI " + request.getRequestURI());
} else {
handler.recordSession(request);
}
///繼續執行下一個執行器
filterChain.doFilter(servletRequest, servletResponse);
}
AuthenticationFilter,該過濾器主要做法:
 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String requestedUrl = ((HttpServletRequest)servletRequest).getServletPath();
boolean isExcludedUrl = false;
//這裡會獲取到xml中的排除需要過濾的URL配置
if (this.excludedRequestUrlPatterns != null && this.excludedRequestUrlPatterns.length > 0) {
Pattern[] arr$ = this.excludedRequestUrlPatterns;
int len$ = arr$.length; for(int i$ = 0; i$ < len$; ++i$) {
Pattern p = arr$[i$];
if (isExcludedUrl = p.matcher(requestedUrl).matches()) {
break;
}
}
} HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//如果當前URL是被排除,不需要校驗cas單點登入的話,直接跳過當前過濾器,進行下一步
if (this.isIgnoreSSO() && isExcludedUrl) {
filterChain.doFilter(request, response);
} else {
//如果當前不被排除在外,檢視白名單URL,也可以直接跳過該過濾器
boolean isWhiteUrl = false;
if (this.whiteRequestUrlPatterns != null && this.whiteRequestUrlPatterns.length > 0) {
Pattern[] arr$ = this.whiteRequestUrlPatterns;
int len$ = arr$.length; for(int i$ = 0; i$ < len$; ++i$) {
Pattern p = arr$[i$];
if (isWhiteUrl = p.matcher(requestedUrl).matches()) {
break;
}
}
} if (isWhiteUrl) {
filterChain.doFilter(request, response);
} else {
//如果都沒匹配上,說明該URL是需要進行校驗檢視的
HttpSession session = request.getSession(false);
//從session中取出改屬性值,檢視當前session是否已經認證過了。如果認證過了了,可以跳過該過濾器
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
//第一次請求的時候,改物件一定為null,因為沒人登入過
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = CommonUtils.safeGetParameter(request, this.getArtifactParameterName());
//檢視是否session中有_const_cas_gateway_該屬性值,第一次登入也沒有
boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
//如果都沒有
if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
String encodedService;
//檢視是否是cas伺服器return回撥我們的這個介面請求,該屬性值在下面,也就是第一次登入的時候,設定的
if (request.getSession().getAttribute("casreturn") != null) {
request.getSession().removeAttribute("casreturn");
if (isExcludedUrl) {
filterChain.doFilter(request, response);
} else {
encodedService = Base64.encodeBase64String(serviceUrl.getBytes());
encodedService = encodedService.replaceAll("[\\s*\t\n\r]", "");
if (!this.SSOLoginUrl.startsWith("https://") && !this.SSOLoginUrl.startsWith("http://")) {
this.SSOLoginUrl = this.getServerName() + (this.getServerName().endsWith("/") ? "" : "/") + this.SSOLoginUrl;
}
//[email protected]這裡----------------------
//一直以為是所有校驗都沒有引數後,在下面才是跳轉到登入頁,,沒想到,直接回調了,並沒有讓使用者去登陸,而是在這裡才去呼叫登入頁
//讓使用者去登陸。大坑
response.sendRedirect(CommonUtils.joinUrl(this.SSOLoginUrl, "nextpage=" + encodedService));
}
} else {
//第一次登入的時候是這裡,他會將你xml中的cas伺服器地址拼接成login登入地址,我們當前請求的URL編碼之後,會被cas登入成功後回撥使用
encodedService = this.SSOServerUrl + "/login?service=" + URLEncoder.encode(serviceUrl, "UTF-8") + "&redirect=true";
//並且設定cas伺服器回撥標識
request.getSession().setAttribute("casreturn", true);
//第一次登入的時候,只能到這裡了,因為ticket引數,或則session中_const_cas_assertion_屬性都沒有,只能去cas伺服器請求登入,
//這裡有個坑,,沒想到在這裡沒有直接出現登入頁,而是呼叫cas伺服器地址後,直接返回來了,而且會在@那裡再去呼叫登入地址
response.sendRedirect(encodedService);
//其他的事情後續就不要再debug了,已經跟我們cas沒有啥關係了,博主,debug了半天越看越懵,才發現是服務在做其他的事情,
// 我們的登入頁面早就已經出現了
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
}

上面的還有一個坑,就是,在使用者登入成功後,回撥我們的地址,第一次並不會帶給我們ticket引數,而且還會走

ncodedService = this.SSOServerUrl + "/login?service=" + URLEncoder.encode(serviceUrl, "UTF-8") + "&redirect=true";
這個邏輯,並且附上casreturn屬性,然後,cas伺服器這回才會把ticket引數返回給我們的介面,剩下的就是下一個過濾器的事情了,慢慢來:

好了,這次有ticket了,我們來看下一個過濾器SSO Ticket Validation Filter

 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//這裡做了點事,是否為代理,博主沒用這個,預設代理為null,返回true
if (this.preFilter(servletRequest, servletResponse, filterChain)) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//獲取ticket請求引數
String ticket = CommonUtils.safeGetParameter(request, this.getArtifactParameterName());
//到這裡了,分為三種情況,
//有ticket,因為你已經登入了,cas伺服器登入成功返回給你了,接下來進行校驗
//無ticket,可能你沒有配置第一個過濾器,溜進來了
//無ticket,ticket已經校驗成功後跳轉回來了,使用者屬性已經設定到session中了,所以這次請求沒有ticket了,不用去校驗
if (CommonUtils.isNotBlank(ticket)) {
if (this.log.isDebugEnabled()) {
this.log.debug("Attempting to validate ticket: " + ticket);
} try {
//開始ticket票據校驗,這才是這個ticket過濾器真正要做的
//constructServiceUrl這個方法不用管,就是拼接一下URL路徑,把我的APPID啥的拼接上去
//validate做了挺多事,請看下一個類註釋,這裡先過去(大概邏輯就是去cas伺服器驗證ticket)
Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
if (this.log.isDebugEnabled()) {
this.log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());
}
//看到這裡沒有,就是在第一個過濾器進行校驗的引數,如果ticket驗證成功,就會往request,及session設定屬性,該屬性就是_const_cas_assertion_
//該屬性值則是一個使用者資訊map
request.setAttribute("_const_cas_assertion_", assertion);
if (this.useSession) {
request.getSession().setAttribute("_const_cas_assertion_", assertion);
}
//空方法,不用管
this.onSuccessfulValidation(request, response, assertion);
//ticket驗證成功後,在進行跳轉,這次是跳到我們自己的請求地址
if (this.redirectAfterValidation) {
this.log.debug("Redirecting after successful ticket validation.");
response.sendRedirect(this.constructServiceUrl(request, response));
return;
}
} catch (TicketValidationException var8) {
response.setStatus(403);
this.log.warn(var8, var8);
this.onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(var8);
} return;
}
} filterChain.doFilter(request, response);
}
}

裡面的ticket驗證邏輯在此:

 public Assertion validate(String ticket, String service) throws TicketValidationException {
//此處是拼接好要呼叫的URL
//http://sso.jxeduyun.com/sso/,該路徑是在web.xml中改ticket過濾器進行配置的SSOServerUrlPrefix
//http://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
String validationUrl = this.constructValidationUrl(ticket, service);
if (this.log.isDebugEnabled()) {
this.log.debug("Constructing validation url: " + validationUrl);
} try {
this.log.debug("Retrieving response from server.");
//這裡不用看,就是發起請求呼叫上面的介面,檢視ticket有效性
String serverResponse = this.retrieveResponseFromServer(new URL(validationUrl), ticket);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
} else {
if (this.log.isDebugEnabled()) {
this.log.debug("Server response: " + serverResponse);
}
//這個不用看了,就是解析返回的cas資料,然後獲取裡面的使用者資訊,並封裝成map
return this.parseResponseFromServer(serverResponse);
}
} catch (MalformedURLException var5) {
throw new TicketValidationException(var5);
}
}

因為ticket驗證成功後並沒有直接到下一個過濾器,而是從新請求了一次,這次不會有ticket引數了,因為session中已經有屬性了,就在前幾個過濾器中進行判斷,在都走一次,然後才會到下面這個過濾器

 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpSession session = request.getSession(false);
Assertion assertion = (Assertion)((Assertion)(session == null ? request.getAttribute("_const_cas_assertion_") : session.getAttribute("_const_cas_assertion_"))); try {
//該過濾器的作用就是,把使用者物件從session中拿出來,放到AssertionHolder裡面,從而在程式碼中獲取物件資訊的時候,
//直接呼叫該物件即可
AssertionHolder.setAssertion(assertion);
filterChain.doFilter(servletRequest, servletResponse);
} finally {
AssertionHolder.clear();
} }

至此,cas的登入流程全部走完,不知道大家看懂多少,花了博主大概一天的時間才把原始碼理解通,ticket返回示例給大家一下,還有程式碼呼叫:

 失敗示例:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationFailure code='INVALID_TICKET'>
ticket 'ST-28699-qdyblKpRwc5LpLk57dRM-sso.jxeduyun.com' not recognized
</cas:authenticationFailure>
</cas:serviceResponse>
成功示例:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>test</cas:user>
<cas:attributes>
<cas:multipleId>test-test-test-test-test</cas:multipleId> <cas:userId>test</cas:userId> <cas:loginName>test</cas:loginName> </cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>

程式碼呼叫示例:

         Assertion assertion = AssertionHolder.getAssertion();
String openId = assertion.getPrincipal().getName();
Map<String, Object> attributes = assertion.getPrincipal().getAttributes();
String userId = attributes.get("userId").toString();
String loginName = attributes.get("loginName").toString();
System.out.println("openId:"+openId);
System.out.println("userId:"+userId);
System.out.println("loginName:"+loginName);

原創不易,轉載請說明出處!謝謝