CAS 5.2.x 单点登录 - 实现原理及源码浅析
上一篇文章簡單介紹了 CAS 5.2.2 在本地開發環境中搭建服務端和客戶端,對單點登錄過程有了一個直觀的認識之后,本篇將探討 CAS 單點登錄的實現原理。
一、Session 和 Cookie
HTTP 是無狀態協議,客戶端與服務端之間的每一次通訊都是獨立的,而會話機制可以讓服務端鑒別每次通訊過程中的客戶端是否是同一個,從而保證業務的關聯性。Session 是服務器使用一種類似于散列表的結構,用來保存用戶會話所需要的信息。Cookie 作為瀏覽器緩存,存儲 Session ID 以到達會話跟蹤的目的。
由于 Cookie 的跨域策略限制,Cookie 攜帶的會話標識無法在域名不同的服務端之間共享。
因此引入 CAS 服務端作為用戶信息鑒別和傳遞中介,達到單點登錄的效果。
二、CAS 流程圖
官方流程圖,地址:https://apereo.github.io/cas/...
瀏覽器與 APP01 服務端
瀏覽器與 APP02 服務端
三、相關源碼
3.1 CAS客戶端
3.1.1 根據是否已登錄進行攔截跳轉
以客戶端攔截器作為入口,對于用戶請求,如果是已經校驗通過的,直接放行:
org.jasig.cas.client.authentication.AuthenticationFilter#doFilter
否則進行重定向:
org.jasig.cas.client.authentication.AuthenticationFilter#doFilter
對于Ajax請求和非Ajax請求的重定向,進行分別處理:
org.jasig.cas.client.authentication.FacesCompatibleAuthenticationRedirectStrategy#redirect
3.1.2 校驗Ticket
如果請求中帶有 Ticket,則進行校驗,校驗成功返回用戶信息:
org.jasig.cas.client.validation.AbstractTicketValidationFilter#doFilter
打斷點得知返回的信息為 XML 格式字符串:
org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#validate
XML 文件內容示例:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationSuccess><cas:user>casuser</cas:user><cas:attributes><cas:credentialType>UsernamePasswordCredential</cas:credentialType><cas:isFromNewLogin>true</cas:isFromNewLogin><cas:authenticationDate>2018-03-25T22:09:49.768+08:00[GMT+08:00]</cas:authenticationDate><cas:authenticationMethod>AcceptUsersAuthenticationHandler</cas:authenticationMethod><cas:successfulAuthenticationHandlers>AcceptUsersAuthenticationHandler</cas:successfulAuthenticationHandlers><cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed></cas:attributes></cas:authenticationSuccess> </cas:serviceResponse>最后將 XML 字符串轉換為對象 org.jasig.cas.client.validation.Assertion,并存儲在 Session 或 Request 中。
3.1.3 重寫Request請求
定義過濾器:
org.jasig.cas.client.util.HttpServletRequestWrapperFilter#doFilter
其中定義 CasHttpServletRequestWrapper,重寫 HttpServletRequestWrapperFilter:
final class CasHttpServletRequestWrapper extends HttpServletRequestWrapper {private final AttributePrincipal principal;CasHttpServletRequestWrapper(final HttpServletRequest request, final AttributePrincipal principal) {super(request);this.principal = principal;}public Principal getUserPrincipal() {return this.principal;}public String getRemoteUser() {return principal != null ? this.principal.getName() : null;}// 省略其他代碼這樣使用以下代碼即可獲取已登錄用戶信息。
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();3.2 CAS服務端
3.2.1 用戶密碼校驗
服務端采用了 Spirng Web Flow,以 login-webflow.xml 為入口:
<action-state id="realSubmit"><evaluate expression="authenticationViaFormAction"/><transition on="warn" to="warn"/><transition on="success" to="sendTicketGrantingTicket"/><transition on="successWithWarnings" to="showAuthenticationWarningMessages"/><transition on="authenticationFailure" to="handleAuthenticationFailure"/><transition on="error" to="initializeLoginForm"/> </action-state>action-state代表一個流程,其中 id 為該流程的標識。
evaluate expression為該流程的實現類。
transition表示對返回結果的處理。
定位到該流程對應的實現類authenticationViaFormAction,可知在項目啟動時實例化了對象AbstractAuthenticationAction:
@ConditionalOnMissingBean(name = "authenticationViaFormAction") @Bean @RefreshScope public Action authenticationViaFormAction() {return new InitialAuthenticationAction(initialAuthenticationAttemptWebflowEventResolver,serviceTicketRequestWebflowEventResolver,adaptiveAuthenticationPolicy); }在頁面上點擊登錄按鈕,進入:
org.apereo.cas.web.flow.actions.AbstractAuthenticationAction#doExecute
org.apereo.cas.authentication.PolicyBasedAuthenticationManager#authenticate
經過層層過濾,得到執行校驗的AcceptUsersAuthenticationHandler和待校驗的UsernamePasswordCredential。
執行校驗,進入
org.apereo.cas.authentication.AcceptUsersAuthenticationHandler#authenticateUsernamePasswordInternal
3.2.2 登錄頁Ticket校驗
在 login-webflow.xml 中定義了 Ticket 校驗流程:
<action-state id="ticketGrantingTicketCheck"><evaluate expression="ticketGrantingTicketCheckAction"/><transition on="notExists" to="gatewayRequestCheck"/><transition on="invalid" to="terminateSession"/><transition on="valid" to="hasServiceCheck"/> </action-state>org.apereo.cas.web.flow.TicketGrantingTicketCheckAction#doExecute
@Override protected Event doExecute(final RequestContext requestContext) {// 從請求中獲取TicketIDfinal String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);if (!StringUtils.hasText(tgtId)) {return new Event(this, NOT_EXISTS);}String eventId = INVALID;try {// 根據TicketID獲取Tciket對象,校驗是否失效final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);if (ticket != null && !ticket.isExpired()) {eventId = VALID;}} catch (final AbstractTicketException e) {LOGGER.trace("Could not retrieve ticket id [{}] from registry.", e.getMessage());}return new Event(this, eventId); }可知 Ticket 存儲在服務端的一個 Map 集合中:
org.apereo.cas.AbstractCentralAuthenticationService#getTicket(java.lang.String, java.lang.Class<T>)
3.2.3 客戶端Ticket校驗
對于從 CAS 客戶端發送過來的 Ticket 校驗請求,則會進入服務端以下代碼:
org.apereo.cas.DefaultCentralAuthenticationService#validateServiceTicket
從 Ticket 倉庫中,根據 TicketID 獲取 Ticket 對象:
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);在同步塊中校驗 Ticket 是否失效,以及是否來自合法的客戶端:
synchronized (serviceTicket) {if (serviceTicket.isExpired()) {LOGGER.info("ServiceTicket [{}] has expired.", serviceTicketId);throw new InvalidTicketException(serviceTicketId);}if (!serviceTicket.isValidFor(service)) {LOGGER.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",serviceTicketId, serviceTicket.getService().getId(), service);throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());} }根據 Ticket 獲取已登錄用戶:
final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot(); final Authentication authentication = getAuthenticationSatisfiedByPolicy(root.getAuthentication(),new ServiceContext(selectedService, registeredService)); final Principal principal = authentication.getPrincipal();最后將用戶信息返回給客戶端。
總結
以上是生活随笔為你收集整理的CAS 5.2.x 单点登录 - 实现原理及源码浅析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Django - app
- 下一篇: Flask的多app应用,多线程如何体现