读书人

CAS 源码分析 (非proxy形式)

发布时间: 2012-11-06 14:07:00 作者: rapoo

CAS 源码分析 (非proxy模式)

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" name="code">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);    }
?

?

?

?

?

?

?

?

读书人网 >开源软件

热点推荐