资讯专栏INFORMATION COLUMN

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

elisa.yang / 1121人阅读

摘要:上一篇文章简单介绍了在本地开发环境中搭建服务端和客户端,对单点登录过程有了一个直观的认识之后,本篇将探讨单点登录的实现原理。因此引入服务端作为用户信息鉴别和传递中介,达到单点登录的效果。为该流程的实现类。表示对返回结果的处理。

上一篇文章简单介绍了 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 ajaxly
        response.setContentType("text/xml");
        response.setStatus(200);

        final PrintWriter writer = response.getWriter();
        writer.write("");
        writer.write(String.format("",
                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 文件内容示例:


    
        casuser
        
            UsernamePasswordCredential
            true
            2018-03-25T22:09:49.768+08:00[GMT+08:00]
            AcceptUsersAuthenticationHandler
            AcceptUsersAuthenticationHandler
            false
            
    

最后将 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 为该流程的标识。
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 list = new ArrayList<>();
    return createHandlerResult(credential, this.principalFactory.createPrincipal(username), list);
}
3.2.2 登录页Ticket校验

在 login-webflow.xml 中定义了 Ticket 校验流程:


    
    
    
    

org.apereo.cas.web.flow.TicketGrantingTicketCheckAction#doExecute

@Override
protected Event doExecute(final RequestContext requestContext) {
    // 从请求中获取TicketID
    final 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)

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();

最后将用户信息返回给客户端。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71024.html

相关文章

  • cas工作原理浅析与总结

    摘要:是大学发起的一个企业级的开源的项目,旨在为应用系统提供一种可靠的单点登录解决方法属于。实现原理是先通过的认证,然后向申请一个针对于的,之后在访问时把申请到的针对于的以参数传递过去。后面的流程与上述流程步骤及以后步骤类似 CAS( Central Authentication Service )是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登...

    warkiz 评论0 收藏0
  • CAS 5.2.x 单点登录 - 搭建服务端和客户端

    摘要:一简介单点登录,简称为,是目前比较流行的企业业务整合的解决方案之一。客户端拦截未认证的用户请求,并重定向至服务端,由服务端对用户身份进行统一认证。三搭建客户端在官方文档中提供了客户端样例,即。 一、简介 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系...

    Lin_YT 评论0 收藏0
  • Python Flask单点登录问题

    摘要:如果一旦加密算法泄露了,攻击者可以在本地建立一个实现了登录接口的假冒父应用,通过绑定来把子应用发起的请求指向本地的假冒父应用,并作出回应。 1.什么是单点登录? 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。客户端持有ID,服务端持有session...

    tuomao 评论0 收藏0
  • 号外:友户通支持企业自有用户中心啦

    摘要:针对这种情况,友户通特定开发了联邦用户中心来支持企业的自有用户中心。友户通支持通过协议使用企业内部的支持协议的用户中心账号进行登录。友户通目前支持标准协议以及友户通自定义协议可供企业集成。 友户通做用友云的用户系统也一年多了,经常听实施、售前等说要私有化部署友户通,原因无非是企业的考虑到用户安全性和单一用户账号的需求。但由于用户管理的复杂性,友户通部署与维护并不容易,因此经常纠结在用户...

    妤锋シ 评论0 收藏0

发表评论

0条评论

elisa.yang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<