日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

CAS 5.2.x 单点登录 - 实现原理及源码浅析

發布時間:2025/3/18 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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 服務端

  • 瀏覽器第一次訪問受保護的 APP01 服務端,由于未經授權而被攔截并重定向到 CAS 服務端。
  • 瀏覽器第一次與 CAS 服務端通訊,鑒權成功后由 CAS 服務端創建全局會話 SSO Session,生成全局會話標識 TGT 并存儲在瀏覽器 Cookie 中。
  • 瀏覽器重定向到 APP01,重寫 URL 地址帶上全局會話標識 TGT。
  • APP01 拿到全局會話標識 TGT 后向 CAS 服務端請求校驗,若校驗成功,則 APP01 會獲取到已經登錄的用戶信息。
  • APP01 創建局部會話 Session,并將 SessionID 存儲到瀏覽器 Cookie 中。
  • 瀏覽器與 APP01 建立會話。
  • 瀏覽器與 APP02 服務端

  • 瀏覽器第一次訪問受保護的 APP02 服務端,由于未經授權而被攔截并重定向到 CAS 服務端。
  • 瀏覽器第二次與 CAS 服務端通訊,CAS 校驗 Cookie 中的全局會話標識 TGT。
  • 瀏覽器重定向到 APP02,重寫 URL 地址帶上全局會話標識 TGT。
  • APP02 拿到全局會話標識 TGT 后向 CAS 服務端請求校驗,若校驗成功,則 APP02 會獲取到已經登錄的用戶信息。
  • APP02 創建局部會話 Session,并將 SessionID 存儲到瀏覽器 Cookie 中。
  • 瀏覽器與 APP02 建立會話。
  • 三、相關源碼

    3.1 CAS客戶端

    3.1.1 根據是否已登錄進行攔截跳轉

    以客戶端攔截器作為入口,對于用戶請求,如果是已經校驗通過的,直接放行:
    org.jasig.cas.client.authentication.AuthenticationFilter#doFilter

    // 不進行攔截的請求地址 if (isRequestUrlExcluded(request)) {logger.debug("Request is ignored.");filterChain.doFilter(request, response);return; }// Session已經登錄 final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; if (assertion != null) {filterChain.doFilter(request, response);return; }// 從請求中獲取ticket final String serviceUrl = constructServiceUrl(request, response); final String ticket = retrieveTicketFromRequest(request); final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {filterChain.doFilter(request, response);return; }

    否則進行重定向:
    org.jasig.cas.client.authentication.AuthenticationFilter#doFilter

    this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);

    對于Ajax請求和非Ajax請求的重定向,進行分別處理:
    org.jasig.cas.client.authentication.FacesCompatibleAuthenticationRedirectStrategy#redirect

    public void redirect(final HttpServletRequest request, final HttpServletResponse response,final String potentialRedirectUrl) throws IOException {if (CommonUtils.isNotBlank(request.getParameter(FACES_PARTIAL_AJAX_PARAMETER))) {// this is an ajax request - redirect ajaxlyresponse.setContentType("text/xml");response.setStatus(200);final PrintWriter writer = response.getWriter();writer.write("<?xml version='1.0' encoding='UTF-8'?>");writer.write(String.format("<partial-response><redirect url=\"%s\"></redirect></partial-response>",potentialRedirectUrl));} else {response.sendRedirect(potentialRedirectUrl);} }

    3.1.2 校驗Ticket

    如果請求中帶有 Ticket,則進行校驗,校驗成功返回用戶信息:
    org.jasig.cas.client.validation.AbstractTicketValidationFilter#doFilter

    final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response)); logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName()); request.setAttribute(CONST_CAS_ASSERTION, assertion);

    打斷點得知返回的信息為 XML 格式字符串:
    org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#validate

    logger.debug("Retrieving response from server."); final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

    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

    @Override protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential,final String originalPassword) throws GeneralSecurityException {if (this.users == null || this.users.isEmpty()) {throw new FailedLoginException("No user can be accepted because none is defined");}// 頁面輸入的用戶名final String username = credential.getUsername();// 根據用戶名取得緩存中的密碼final String cachedPassword = this.users.get(username);if (cachedPassword == null) {LOGGER.debug("[{}] was not found in the map.", username);throw new AccountNotFoundException(username + " not found in backing map.");}// 校驗緩存中的密碼和用戶輸入的密碼是否一致if (!StringUtils.equals(credential.getPassword(), cachedPassword)) {throw new FailedLoginException();}final List<MessageDescriptor> list = new ArrayList<>();return createHandlerResult(credential, this.principalFactory.createPrincipal(username), list); }

    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 单点登录 - 实现原理及源码浅析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。