CAS 源码分析 (非proxy形式)
一、CAS 基本原理 (3,4,5,9.2,9.3是主要步骤)
第一次访问:
1. 浏览器
发起访问WebAPP 请求: http://www.web.com/app
2. 客户端
AuthenticationFilter Filter 发现Session中无 Assertion,且URL中无 ticket 变量。生成 service url 变量,并重定向到: https://www.cas-server.com/cas/login?service=http://www.web.com/app
3. CAS server
生成 Login ticket, service 对象,并展示 login 页面,默认提供 username / password 给用户验证。
4. CAS server
端,用户输入 username / password 验证,若通过则生成TGT,存入服务器段(默认为 Map 类型的 cache),同时将TGT id 作为 content创建 cookie 并发送到浏览器。
5. CAS server
端通过TGT 生成service ticket. 重定向到 http://www.web.com/app?ticket=ST-xxx
6. 客户端
访问 http://www.web.com/app?ticket=ST-xxx
7. 客户端
AuthenticationFilter Filter 发现URL中有 ticket, 跳过 AuthenticationFilter过滤器,到达 Cas20ProxyReceivingTicketValidationFilter过滤器。
8. 客户端
生成验证 service url: http://www.web.com/app
9. 客户端
Cas20ProxyReceivingTicketValidationFilter 过滤器,使用6处的ticket 与8处的 service 作为参数验证。
9.1 客户端
生成验证 servlet: https://www.cas-server.com/cas/serviceValidate?ticket=ST-xxx&service=http://www.web.com/app
9.2 客户端
通过HttpClient访问 9.1 的 url
注:AbstractUrlBasedTicketValidator.java line 207,如果是用CAS ptotocol验证,则第二个参数 ticket无用。
得到如下形式的 response:
546 <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> 547 <cas:authenticationSuccess> 548 <cas:user>jack</cas:user> 549 550 551 </cas:authenticationSuccess> 552 </cas:serviceResponse>
9.3 客户端 解析 response 字符串,生成 assertion (包含 username, validate info 等)
9.4 客户端 设置 assertion 为 request 的 _const_cas_assertion_ 属性。
9.5 客户端
如果设置了重定向属性,则重定向到 http://www.web.com/app -- 转步骤10
否则继续执行以后的 filter,通过servlet 访问 http://www.web.com/app 服务,结束CAS的验证。
用户已成功登录,非第一次访问:
10. 客户端 通过重定向访问 http://www.web.com/app
11. 客户端 AuthenticationFilter Filter 发现 session 中有Assertion, 结束本过滤器,转移到下一个过滤器 Cas20ProxyReceivingTicketValidationFilter.
12. 客户段 Cas20ProxyReceivingTicketValidationFilter 发现 本次访问的URL 无 ticket,结束本次过滤,转移到下一个过滤器,继续执行以后的 filter,通过servlet 访问 http://www.web.com/app 服务。
二 源码解析
1. 客户端 web.xml 片段:
... <filter> <filter-name>CAS Authentication Filter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>https://www.colorcc.com:8443/cas/login</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> </filter> <filter> <filter-name>CAS Validation Filter</filter-name> <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>https://www.colorcc.com:8443/cas</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> <!-- <init-param> <param-name>redirectAfterValidation</param-name> <param-value>false</param-value> </init-param> --> </filter> <filter-mapping> <filter-name>CAS Authentication Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>CAS Validation Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ...
2. AuthenticationFilter 代码:
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; // 如果 session 中有 assertion,则结束 authentication 过滤器,直接跳到下一个过滤器 if (assertion != null) { filterChain.doFilter(request, response); return; } // 2.1 如果 session 中无 assertion, 则构造 service, 如 http://www.web.com/a1 final String serviceUrl = constructServiceUrl(request, response); final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName()); final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); // 如果 request 中有 ticke变量,则结束本过滤器,直接跳到下一个过滤器 if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { filterChain.doFilter(request, response); return; } final String modifiedServiceUrl; log.debug("no ticket and no assertion found"); if (this.gateway) { log.debug("setting gateway attribute in session"); modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; } if (log.isDebugEnabled()) { log.debug("Constructed service url: " + modifiedServiceUrl); } // 2.2 否则构造重定向 URL, 其中 casServerLoginUrl 为 web.xml 中 filter 配置,eg: https://www.cas-server.com/cas/login?service=http://www.web.com/a1 final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); if (log.isDebugEnabled()) { log.debug("redirecting to \"" + urlToRedirectTo + "\""); } // 2.3 重定向到 CAS server response.sendRedirect(urlToRedirectTo); }
2.1 构造 service url: http://www.web.com/a1
protected final String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response) { return CommonUtils.constructServiceUrl(request, response, this.service, this.serverName, this.artifactParameterName, this.encodeServiceUrl); }
3. 重定向URL: https://www.cas-server.com/cas/login?service=http://www.web.com/a1, 其中 cas server的 web.xml:
<servlet> <servlet-name>cas</servlet-name> <servlet-class> org.jasig.cas.web.init.SafeDispatcherServlet </servlet-class> <init-param> <param-name>publishContext</param-name> <param-value>false</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>
3.1 SafeDispatcherServlet 使用 Spring DispatcherServlet 作为 delegate
public final class SafeDispatcherServlet extends HttpServlet { // 定义 Spring DispatcherServlet 作为 delegate private DispatcherServlet delegate = new DispatcherServlet(); // 使用 delegate 初始化 servlet public void init(final ServletConfig config) { try { this.delegate.init(config); } catch (final Throwable t) { ... // 使用 delegate 的 service 执行 web 操作 public void service(final ServletRequest req, final ServletResponse resp) throws ServletException, IOException { if (this.initSuccess) { this.delegate.service(req, resp); } else { throw new ApplicationContextException( "Unable to initialize application context."); } }
3.2 cas-servlet.xml 配置文件如下, 可以看到 login 对应的 webflow 为: login-webflow.xml
<webflow:flow-registry id="flowRegistry" flow-builder-services="builder"> <webflow:flow-location path="/WEB-INF/login-webflow.xml" id="login"/> </webflow:flow-registry>
3.3 根据 login-webflow.xml 配置文件(结合 cas-servlet.xml):
<on-start> <evaluate expression="initialFlowSetupAction" /> </on-start> <bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction" p:argumentExtractors-ref="argumentExtractors" p:warnCookieGenerator-ref="warnCookieGenerator" p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"/>
3.4 InitialFlowSetupAction
protected Event doExecute(final RequestContext context) throws Exception { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); if (!this.pathPopulated) { final String contextPath = context.getExternalContext().getContextPath(); final String cookiePath = StringUtils.hasText(contextPath) ? contextPath + "/" : "/"; logger.info("Setting path for cookies to: " + cookiePath); this.warnCookieGenerator.setCookiePath(cookiePath); this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath); this.pathPopulated = true; } // 给 FlowScope 的设置 ticketGrantingTicketId, warnCookieValue 参数 context.getFlowScope().put( "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request)); context.getFlowScope().put("warnCookieValue", Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request))); // 3.4.1 抽取 service 参数 final Service service = WebUtils.getService(this.argumentExtractors, context); if (service != null && logger.isDebugEnabled()) { logger.debug("Placing service in FlowScope: " + service.getId()); } // 给 FlowScope 的设置 service 参数 context.getFlowScope().put("service", service); return result("success"); }
3.4.1 WebApplicationService.getService
public static WebApplicationService getService( final List<ArgumentExtractor> argumentExtractors, final HttpServletRequest request) { for (final ArgumentExtractor argumentExtractor : argumentExtractors) { // 3.4.1.1 通过配置的 argumentExtractor 抽取 service final WebApplicationService service = argumentExtractor.extractService(request); if (service != null) { return service; } } return null; }
3.4.1.1 CasArgumentExtractor 代码
public final class CasArgumentExtractor extends AbstractSingleSignOutEnabledArgumentExtractor { public final WebApplicationService extractServiceInternal(final HttpServletRequest request) { return SimpleWebApplicationServiceImpl.createServiceFrom(request, getHttpClientIfSingleSignOutEnabled()); } } // SimpleWebApplicationServiceImpl private static final String CONST_PARAM_SERVICE = "service"; private static final String CONST_PARAM_TARGET_SERVICE = "targetService"; private static final String CONST_PARAM_TICKET = "ticket"; private static final String CONST_PARAM_METHOD = "method"; public static SimpleWebApplicationServiceImpl createServiceFrom( final HttpServletRequest request, final HttpClient httpClient) { final String targetService = request .getParameter(CONST_PARAM_TARGET_SERVICE); final String method = request.getParameter(CONST_PARAM_METHOD); final String serviceToUse = StringUtils.hasText(targetService) ? targetService : request.getParameter(CONST_PARAM_SERVICE); 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) ? ResponseType.POST : ResponseType.REDIRECT, httpClient); } private SimpleWebApplicationServiceImpl(final String id, final String originalUrl, final String artifactId, final ResponseType responseType, final HttpClient httpClient) { super(id, originalUrl, artifactId, httpClient); this.responseType = responseType; } protected AbstractWebApplicationService(final String id, final String originalUrl, final String artifactId, final HttpClient httpClient) { this.id = id; this.originalUrl = originalUrl; this.artifactId = artifactId; this.httpClient = httpClient; }
3. Cas20ProxyReceivingTicketValidationFilter 及 AbstractTicketValidationFilter代码:
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { if (!preFilter(servletRequest, servletResponse, filterChain)) { return; } final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); // 如果 URL 中包含 ticket 参数,则执行 service 验证工作 if (CommonUtils.isNotBlank(ticket)) { if (log.isDebugEnabled()) { log.debug("Attempting to validate ticket: " + ticket); } try { final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response)); if (log.isDebugEnabled()) { log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName()); } request.setAttribute(CONST_CAS_ASSERTION, assertion); if (this.useSession) { request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion); } onSuccessfulValidation(request, response, assertion); if (this.redirectAfterValidation) { log. debug("Redirecting after successful ticket validation."); response.sendRedirect(constructServiceUrl(request, response)); return; } } catch (final TicketValidationException e) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); log.warn(e, e); onFailedValidation(request, response); if (this.exceptionOnValidationFailure) { throw new ServletException(e); } return; } } // 如果不包含 ticket, 直接跳过CAS Filter验证,继续其他 filter 或 web app 操作 filterChain.doFilter(request, response); }