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

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

一、Session 和 Cookie

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

bV6Urj?w=551&h=582

由于 Cookie 的跨域策略限制,Cookie 携带的会话标识无法在域名不同的服务端之间共享。

因此引入 CAS 服务端作为用户信息鉴别和传递中介,达到单点登录的效果。

二、CAS 流程图

bV6UtE?w=1230&h=2020

浏览器与 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("<?xml version='1.0' encoding='UTF-8'?>");

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 中。

bV6Uu1?w=739&h=339

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

bV6Uvn?w=1699&h=602

经过层层过滤,得到执行校验的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)

bV6UvO?w=1628&h=751

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值