HTTP是无状态协议,客户端与服务端之间的每次通信都是独立的,而会话机制可以让服务端鉴别每次通讯过程中的客户端是否是同一个,从而保证业务的关联性。 Session是服务器使用一种类似于散列表的结构,用来保存用户会话所需要的信息.Cookie作为浏览器缓存,存储Session ID以达到会话跟踪的目的。
由于Cookie的跨域策略限制,Cookie携带的会话标识无法在域名不同的服务端之间共享。
因此引入CAS服务端作为用户信息鉴别和传递中介,达到单点登录的效果。
官方流程图,地址:https://apereo.github.io/cas/ ...
浏览器与APP01服务端
浏览器与APP02服务端
以客户端拦截器作为入口,对于用户请求,如果是已经校验通过的,直接放行:
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);
}
}
如果请求中带有 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 文件内容示例:
'http://www.yale.edu/tp/cas'>
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 中。
定义过滤器:
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();
服务端采用了 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 list = new ArrayList<>();
return createHandlerResult(credential, this.principalFactory.createPrincipal(username), list);
}
在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) {
// 从请求中获取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
对于从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();
最后将用户信息返回给客户端。