cas|cas client登录流程源码分析

一.前期准备
1.cas源码版本

4.1.11-SNAPSHOT

2.服务端
http://www.eric.cas.server.com:8080/cas-server

3.cas client
http://www.eric.cas.client.com:8081/cas/index.do

4.cas client web.xml配置
Archetype Created Web Applicationcas-client-4.x org.springframework.web.servlet.DispatcherServlet 1cas-client-4.x / org.springframework.web.context.ContextLoaderListener contextConfigLocationclasspath*:/applicationContext.xml org.jasig.cas.client.session.SingleSignOutHttpSessionListener CAS Single Sign Out Filter org.jasig.cas.client.session.SingleSignOutFilter casServerUrlPrefixhttp://www.eric.cas.server.com:8080/ CAS Single Sign Out Filter /* CAS Authentication Filter org.jasig.cas.client.authentication.AuthenticationFilter casServerLoginUrlhttp://www.eric.cas.server.com:8080/login serverNamehttp://www.eric.cas.client.com:8081 useSessiontrue redirectAfterValidationtrue CAS Authentication Filter /cas/* CAS Validation Filter org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter casServerUrlPrefixhttp://www.eric.cas.server.com:8080/ serverNamehttp://www.eric.cas.client.com:8081 CAS Validation Filter/cas/* CAS HttpServletRequest Wrapper Filter org.jasig.cas.client.util.HttpServletRequestWrapperFilter CAS HttpServletRequest Wrapper Filter /* CAS Assertion Thread Local Filter org.jasig.cas.client.util.AssertionThreadLocalFilter CAS Assertion Thread Local Filter /*

登录流程
当从浏览器访问配置了单点登录的应用系统时(http://www.eric.cas.client.com:8081/cas/index.do),
请求第一次到达SingleSignOutFilter过滤器
一. SingleSignOutFilter SingleSignOutFilter的doFilter方法如下:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; if(!this.handlerInitialized.getAndSet(true)) { HANDLER.init(); } // 处理请求方法 if(HANDLER.process(request, response)) { filterChain.doFilter(servletRequest, servletResponse); }}

可以看到SingleSignOutFilter中处理请求的主要方法是
HANDLER.process(request, response)

实际调用的是SingleSignOutHandler的process方法
public boolean process(HttpServletRequest request, HttpServletResponse response) { // 是否是带有token参数的请求 如ticket参数 if(this.isTokenRequest(request)) { this.logger.trace("Received a token request"); // 记录session this.recordSession(request); return true; } else if(this.isBackChannelLogoutRequest(request)) { // 登出请求销毁session this.logger.trace("Received a back channel logout request"); this.destroySession(request); return false; } else if(this.isFrontChannelLogoutRequest(request)) { this.logger.trace("Received a front channel logout request"); // 登出请求销毁session this.destroySession(request); String redirectionUrl = this.computeRedirectionToServer(request); if(redirectionUrl != null) { CommonUtils.sendRedirect(response, redirectionUrl); }return false; } else { this.logger.trace("Ignoring URI for logout: {}", request.getRequestURI()); return true; } }

以上源码可以看到,process方法主要是用于单点登录时记录登录的session信息,单点登出时,销毁记录的session。
由于是第一次请求,直接返回true,跳转到下一个过滤器AuthenticationFilter
二. AuthenticationFilter AuthenticationFilter的doFilter方法如下:
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; if(this.isRequestUrlExcluded(request)) { this.logger.debug("Request is ignored."); filterChain.doFilter(request, response); } else { //判断登录session是否存在 HttpSession session = request.getSession(false); //从session中获取名为"_const_cas_assertion_"的Assertion Assertion assertion = session != null?(Assertion)session.getAttribute("_const_cas_assertion_"):null; if(assertion != null) { //登录session存在 则执行下一个filter filterChain.doFilter(request, response); } else { String serviceUrl = this.constructServiceUrl(request, response); String ticket = this.retrieveTicketFromRequest(request); boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); if(!CommonUtils.isNotBlank(ticket) && !wasGatewayed) { this.logger.debug("no ticket and no assertion found"); String modifiedServiceUrl; if(this.gateway) { this.logger.debug("setting gateway attribute in session"); modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; }this.logger.debug("Constructed service url: {}", modifiedServiceUrl); String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); this.logger.debug("redirecting to \"{}\"", urlToRedirectTo); this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo); } else { //如果ticket不为空,本过滤器处理完成,处理下个过滤器 filterChain.doFilter(request, response); } } } }

由以上源码可知,当请求到达之后执行以下操作:
  1. 首先判断当前请求是否需要过滤,不需要则直接跳转到下一个过滤器。
  2. 从session中获取名为“const_cas_assertion”的assertion对象,判断assertion是否存在,如果存在,说明已经登录,执行下一个过滤器。如果不存在,执行第3步。
  3. 生成serviceUrl(http://www.eric.cas.client.com:8081/cas/index.do),并且从request中获取票据参数ticket,判断ticket是否为空,如果不为空执行下一个过滤器。如果为空,执行第4步。
  4. 生成重定向URL(http://www.eric.cas.server.com:8080/cas-server/login?service=http://www.eric.cas.client.com:8081/cas/index.do)。
  5. 执行重定向,跳转到单点登录服务器,显示登录页面
浏览器登录页面
  1. 登录页面输入用户名和密码,执行登录操作,请求到达cas server,cas server做登录校验之后,生成登录所需要的ticket并重定向到cas client,具体重定向地址如下
http://www.eric.cas.client.com:8081/cas/index.do?ticket=ST-1-alt4ccCXxjOamzWU4Hid-cas01.example.org

此时已经拿到了单点登录的临时票据Service Ticket,简称ST。
带有ST参数的请求重新到达cas client,还是先到达SingleSignOutFilter,此时已经拿到登录的ST,则保存登录的票据信息到session,然后执行下一个拦截器AuthenticationFilter
if(this.isTokenRequest(request)) { this.logger.trace("Received a token request"); this.recordSession(request); return true; }

private void recordSession(HttpServletRequest request) { HttpSession session = request.getSession(this.eagerlyCreateSessions); if(session == null) { this.logger.debug("No session currently exists (and none created).Cannot record session information for single sign out."); } else { String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters); this.logger.debug("Recording session for token {}", token); try { this.sessionMappingStorage.removeBySessionById(session.getId()); } catch (Exception var5) { ; }this.sessionMappingStorage.addSessionById(token, session); } }

带有ST参数的请求到达AuthenticationFilter,此时assertion对象仍然为空但是ticket不为空,则直接跳转到下一个过滤器Cas20ProxyReceivingTicketValidationFilter
三. Cas20ProxyReceivingTicketValidationFilter Cas20ProxyReceivingTicketValidationFilter继承了AbstractTicketValidationFilter类,doFilter方法如下
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { if(this.preFilter(servletRequest, servletResponse, filterChain)) { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; /从request中获取ST参数 String ticket = this.retrieveTicketFromRequest(request); if(CommonUtils.isNotBlank(ticket)) { this.logger.debug("Attempting to validate ticket: {}", ticket); try { //验证ticket并产生Assertion对象,错误抛出TicketValidationException异常 Assertion e = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response)); this.logger.debug("Successfully authenticated user: {}", e.getPrincipal().getName()); //验证成功 保存认证用户信息到session request.setAttribute("_const_cas_assertion_", e); if(this.useSession) { request.getSession().setAttribute("_const_cas_assertion_", e); }this.onSuccessfulValidation(request, response, e); //跳转到请求地址 if(this.redirectAfterValidation) { this.logger.debug("Redirecting after successful ticket validation."); response.sendRedirect(this.constructServiceUrl(request, response)); return; } } catch (TicketValidationException var8) { this.logger.debug(var8.getMessage(), var8); this.onFailedValidation(request, response); if(this.exceptionOnValidationFailure) { throw new ServletException(var8); }response.sendError(403, var8.getMessage()); return; } }filterChain.doFilter(request, response); } }

Cas20ProxyReceivingTicketValidationFilter,执行以下操作:
从request获取ticket参数,如果ticket为空,继续处理下一个过滤器。如果参数不为空,验证ticket参数的合法性。
验证ticket,TicketValidator的validate方法通过httpClient访问CAS服务器端
http://www.eric.cas.server.com:8080/serviceValidate?ticket=ST-6-okgjpFYrNhfVYbCx9xYQ-cas01.example.org&service=http%3A%2F%2Fwww.eric.cas.client.com%3A8081%2Fcas%2Findex.do

验证ticket是否正确,并返回assertion对象。
如果验证失败,抛出异常,跳转到错误页面。
如果验证成功,则将Assertion对象保存到session中,s,继续处理下一个过滤器。
request.getSession().setAttribute("_const_cas_assertion_", e);

单点登录流程结束
服务端具体验证ST流程
补充上面说的cas client请求cas server验证ticket的具体逻辑
验证请求如下:
http://www.eric.cas.server.com:8080/serviceValidate?ticket=ST-6-okgjpFYrNhfVYbCx9xYQ-cas01.example.org&service=http%3A%2F%2Fwww.eric.cas.client.com%3A8081%2Fcas%2Findex.do

查看服务端cas-servlet.xml文件配置
serviceValidateControllerproxyValidateControllerv3ServiceValidateControllerv3ProxyValidateControllerlegacyValidateControllerproxyController【cas|cas client登录流程源码分析】passThroughController

可以看到cas client发送的ST验证请求由serviceValidateController负责处理
@Override protected final ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) throws Exception { final WebApplicationService service = this.argumentExtractor.extractService(request); final String serviceTicketId = service != null ? service.getArtifactId() : null; if (service == null || serviceTicketId == null) { logger.debug("Could not identify service and/or service ticket for service: [{}]", service); return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_REQUEST, CasProtocolConstants.ERROR_CODE_INVALID_REQUEST, null); }try { final Credential serviceCredential = getServiceCredentialsFromRequest(service, request); TicketGrantingTicket proxyGrantingTicketId = null; if (serviceCredential != null) { try { proxyGrantingTicketId = this.centralAuthenticationService.delegateTicketGrantingTicket(serviceTicketId, serviceCredential); logger.debug("Generated PGT [{}] off of service ticket [{}] and credential [{}]", proxyGrantingTicketId.getId(), serviceTicketId, serviceCredential); } catch (final AuthenticationException e) { logger.info("Failed to authenticate service credential {}", serviceCredential); } catch (final TicketException e) { logger.error("Failed to create proxy granting ticket for {}", serviceCredential, e); }if (proxyGrantingTicketId == null) { return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK, CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK, new Object[] {serviceCredential.getId()}); } } // 验证ST是否正确并且生成Assertion对象 final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service); final ValidationSpecification validationSpecification = this.getCommandClass(); final ServletRequestDataBinder binder = new ServletRequestDataBinder(validationSpecification, "validationSpecification"); initBinder(request, binder); binder.bind(request); if (!validationSpecification.isSatisfiedBy(assertion)) { logger.debug("Service ticket [{}] does not satisfy validation specification.", serviceTicketId); return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_TICKET, CasProtocolConstants.ERROR_CODE_INVALID_TICKET, null); }String proxyIou = null; if (serviceCredential != null && this.proxyHandler.canHandle(serviceCredential)) { proxyIou = this.proxyHandler.handle(serviceCredential, proxyGrantingTicketId); if (StringUtils.isEmpty(proxyIou)) { return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK, CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK, new Object[] {serviceCredential.getId()}); } }onSuccessfulValidation(serviceTicketId, assertion); logger.debug("Successfully validated service ticket {} for service [{}]", serviceTicketId, service.getId()); return generateSuccessView(assertion, proxyIou, service, proxyGrantingTicketId); } catch (final TicketValidationException e) { final String code = e.getCode(); return generateErrorView(code, code, new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()}); } catch (final TicketException te) { return generateErrorView(te.getCode(), te.getCode(), new Object[] {serviceTicketId}); } catch (final UnauthorizedProxyingException e) { return generateErrorView(e.getMessage(), e.getMessage(), new Object[] {service.getId()}); } catch (final UnauthorizedServiceException e) { return generateErrorView(e.getMessage(), e.getMessage(), null); } }

查看ST验证的主要方法CentralAuthenticationServiceImpl类的validateServiceTicket方法
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws TicketException { final RegisteredService registeredService = this.servicesManager.findServiceBy(service); verifyRegisteredServiceProperties(registeredService, service); // 从缓存中查看ST是否存在 final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class); if (serviceTicket == null) { logger.info("Service ticket [{}] does not exist.", serviceTicketId); throw new InvalidTicketException(serviceTicketId); }try { synchronized (serviceTicket) { // ST是否过期 if (serviceTicket.isExpired()) { logger.info("ServiceTicket [{}] has expired.", serviceTicketId); throw new InvalidTicketException(serviceTicketId); } // ST是否合法 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()); } }final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot(); final Authentication authentication = getAuthenticationSatisfiedByPolicy( root, new ServiceContext(serviceTicket.getService(), registeredService)); final Principal principal = authentication.getPrincipal(); final AttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy(); logger.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService); @SuppressWarnings("unchecked") final Map attributesToRelease = attributePolicy != null ? attributePolicy.getAttributes(principal) : Collections.EMPTY_MAP; final String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, service); final Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease); final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication); builder.setPrincipal(modifiedPrincipal); return new ImmutableAssertion( builder.build(), serviceTicket.getGrantingTicket().getChainedAuthentications(), serviceTicket.getService(), serviceTicket.isFromNewLogin()); } finally { if (serviceTicket.isExpired()) { this.ticketRegistry.deleteTicket(serviceTicketId); } } }

这里主要看下serviceTicket.isValidFor(service)方法
@Override public boolean isValidFor(final Service serviceToValidate) { updateState(); return serviceToValidate.matches(this.service); }

该方法实际调用的是AbstractWebApplicationService类的matches方法
@Override public boolean matches(final Service service) { try { final String thisUrl = URLDecoder.decode(this.id, "UTF-8"); final String serviceUrl = URLDecoder.decode(service.getId(), "UTF-8"); logger.trace("Decoded urls and comparing [{}] with [{}]", thisUrl, serviceUrl); return thisUrl.equalsIgnoreCase(serviceUrl); } catch (final Exception e) { logger.error(e.getMessage(), e); } return false; }

可以看到验证ST就是验证的登录请求保存的service的ID和验证请求的service的ID是否相等,相等就认为ST合法。
这里也可以修改验证逻辑,添加自己的验证逻辑,但前提是确保ST验证的合法性,安全性。
这里的Service到底是什么呢,其实Service就是cas server把收到的客户端请求封装之后产生的对象
具体配置文件是argumentExtractorsConfiguration.xml
Argument Extractors are what are used to translate HTTP requests into requests of the appropriate protocol (i.e. CAS, SAML, SAML2, OpenId, etc.). By default, only CAS is enabled.

CasArgumentExtractor类
public final class CasArgumentExtractor extends AbstractArgumentExtractor {@Override public WebApplicationService extractServiceInternal(final HttpServletRequest request) { return SimpleWebApplicationServiceImpl.createServiceFrom(request); } }

可以看到Service接口的实例是SimpleWebApplicationServiceImpl类的对象
/** * Creates the service from the request. * * @param request the request * @return the simple web application service impl */ public static SimpleWebApplicationServiceImpl createServiceFrom( final HttpServletRequest request) { final String targetService = request.getParameter(CONST_PARAM_TARGET_SERVICE); final String service = request.getParameter(CONST_PARAM_SERVICE); final String serviceAttribute = (String) request.getAttribute(CONST_PARAM_SERVICE); final String method = request.getParameter(CONST_PARAM_METHOD); final String serviceToUse; if (StringUtils.hasText(targetService)) { serviceToUse = targetService; } else if (StringUtils.hasText(service)) { serviceToUse = service; } else { serviceToUse = serviceAttribute; }if (!StringUtils.hasText(serviceToUse)) { return null; }final String id = cleanupUrl(serviceToUse); final String artifactId = request.getParameter(CONST_PARAM_TICKET); return new SimpleWebApplicationServiceImpl(id, serviceToUse, artifactId, "POST".equals(method) ? Response.ResponseType.POST : Response.ResponseType.REDIRECT); }

这里可以看到service的封装过程,由此可见登录时传递给cas server的service参数和ST验证时传递给cas server的service参数必须相同,ST才能验证成功
服务端验证ST流程结束。

    推荐阅读