上一篇文章简单介绍了 CAS 5.2.2 在本地开发环境中搭建服务端和客户端,对单点登录过程有了一个直观的认识之后,本篇将探讨 CAS 单点登录的实现原理。

一、Session 和 Cookie

HTTP 是无状态协议,客户端与服务端之间的每一次通讯都是独立的,而会话机制可以让服务端鉴别每次通讯过程中的客户端是否是同一个,从而保证业务的关联性。Session 是服务器使用一种类似于散列表的结构,用来保存用户会话所需要的信息。Cookie 作为浏览器缓存,存储 Session ID 以到达会话跟踪的目的。

由于 Cookie 的跨域策略限制,Cookie 携带的会话标识无法在域名不同的服务端之间共享。
因此引入 CAS 服务端作为用户信息鉴别和传递中介,达到单点登录的效果。

二、CAS 流程图

官方流程图,地址:https://apereo.github.io/cas/...

浏览器与 APP01 服务端

  1. 浏览器第一次访问受保护的 APP01 服务端,由于未经授权而被拦截并重定向到 CAS 服务端。
  2. 浏览器第一次与 CAS 服务端通讯,鉴权成功后由 CAS 服务端创建全局会话 SSO Session,生成全局会话标识 TGT 并存储在浏览器 Cookie 中。
  3. 浏览器重定向到 APP01,重写 URL 地址带上全局会话标识 TGT。
  4. APP01 拿到全局会话标识 TGT 后向 CAS 服务端请求校验,若校验成功,则 APP01 会获取到已经登录的用户信息。
  5. APP01 创建局部会话 Session,并将 SessionID 存储到浏览器 Cookie 中。
  6. 浏览器与 APP01 建立会话。

浏览器与 APP02 服务端

  1. 浏览器第一次访问受保护的 APP02 服务端,由于未经授权而被拦截并重定向到 CAS 服务端。
  2. 浏览器第二次与 CAS 服务端通讯,CAS 校验 Cookie 中的全局会话标识 TGT。
  3. 浏览器重定向到 APP02,重写 URL 地址带上全局会话标识 TGT。
  4. APP02 拿到全局会话标识 TGT 后向 CAS 服务端请求校验,若校验成功,则 APP02 会获取到已经登录的用户信息。
  5. APP02 创建局部会话 Session,并将 SessionID 存储到浏览器 Cookie 中。
  6. 浏览器与 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 单点登录 - 实现原理及源码浅析相关推荐

  1. (附源码)Spring Boot 框架整合 OAuth2 实现单点登录 SSO 详细完整源码教程!

    1.  前言 技术这东西吧,看别人写的好像很简单似的,到自己去写的时候就各种问题,"一看就会,一做就错".网上关于实现SSO的文章一大堆,但是当你真的照着写的时候就会发现根本不是那 ...

  2. 单点登录(4):单点登录实现(附源码)

    1.修改host实现跨域的单点登录 由于需要演示跨域,需要先进行域名设置.不会的先看我的另一篇文章mac配置自定义域名:设置如下: 127.0.0.1 www.sso.com 127.0.0.1 ww ...

  3. 一文搞懂主流的扫码登录技术原理(附源码)

    点击上方[全栈开发者社区]→右上角[...]→[设为星标⭐] 1.引言 扫码登录这个功能,最早应该是微信的PC端开始搞,虽然有点反人类的功能(不扫码也没别的方式登录),但不得不说还是很酷的. 下面这张 ...

  4. 基于IdentityServer4的OIDC实现单点登录(SSO)原理简析

     # 写在前面 IdentityServer4的学习断断续续,兜兜转转,走了不少弯路,也花了不少时间.可能是因为没有阅读源码,也没有特别系统的学习资料,相关文章很多园子里的大佬都有涉及,有系列文章 ...

  5. 单点登录(原理与代码)

    文章目录 一.单点登录简介 SSO 原理(单点登录的过程) SSO 实现技术 SSO 常见方案 二.手写单点登录系统架构(代码篇) 三.单点登录详介(原理篇) 1.登录 2.注销 单点登陆的具体实现: ...

  6. 浅析C#中单点登录的原理和使用

    是单点登录? 我想肯定有一部分人"望文生义"的认为单点登录就是一个用户只能在一处登录,其实这是错误的理解(我记得我第一次也是这么理解的). 单点登录指的是多个子系统只需要登录一个, ...

  7. 单点登录简单原理(应用多系统)

    单点登录原理与简单实现 1.http无状态协议  web应用采用browser/server架构,http作为通信协议.http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求 ...

  8. Oauth2.0实现单点登录的原理流程,这次总该懂了!

    单点登录是多域名企业站点流行的登录方式.本文以现实生活场景辅助理解,力争彻底理清 OAuth2.0 实现单点登录的原理流程.同时总结了权限控制的实现方案,及其在微服务架构中的应用. 1 什么是单点登录 ...

  9. Oauth2.0实现单点登录的原理流程

    Oauth2.0实现单点登录的原理流程 1.什么是单点登录 2.OAuth2 认证授权的原理流程 3.基于 SpringBoot 实现认证/授权 4.综合运用 1.什么是单点登录 1.1 多点登录 传 ...

最新文章

  1. 代码运行时间测试C++
  2. MYSQL 查询数据排序数据和分组数据
  3. 新书推荐 |《PostgreSQL实战》出版
  4. 给互联网巨头“搬砖”的人
  5. 华为发布岳云鹏手机_华为P40系列发布,再谈手机隐私安全重要性
  6. Android NDK 环境搭建
  7. 百度地图API禁用点击景点弹出详细信息的方法
  8. 从工作的角度看 CV/NLP/推荐系统选哪个?
  9. java线程条件变量_使用条件变量(多线程笔记)
  10. phpcmsV9导航栏目点击跳转始终是localhost首页 bug - 分析篇
  11. java调用android_Java及Android中常用链式调用写法简单示例
  12. 竞品分析 | 不背单词、百词斩
  13. 光纤交换机与普通交换机的区别
  14. php 获取array的长度_php中获取数组长度的方法
  15. A15.从零开始前后端react+flask - 将前后端联系起来
  16. 云科技时代力作:《读懂新基建,数字技术带来全民机遇》上市
  17. java高级架构师工资多少啊,附源代码
  18. 信息技术的技术趋势和未来展望
  19. 比较IC卡、ID卡、M1卡、CPU卡它们之间有什么区别?
  20. 标准方程法(正规方程法)

热门文章

  1. ASP.NET中实现复用代码自定义用户控件UserControl的使用
  2. Java中使用Jedis连接Redis对String进行操作的常用命令
  3. Winform中使用控件的Dock属性设计窗体布局,使不随窗体缩放而改变
  4. C#中控制窗体的显示与隐藏
  5. 政府项目需要注意的事项
  6. 在软盘上找不到id地址标记_记录一下我的群辉使用腾讯云域名解析IPv6地址
  7. 神策“营销云·微信生态”全面开放,限时免费申请通道开启!
  8. 神策数据斩获大数据星河奖,首发用户行为分析标准
  9. Java中String和byte[]间的转换浅析
  10. pycharm如何放大字体和缩小字体