SSO单点登录系列1:cas客户端源码分析cas-client-java-2.1.1.jar

  1. 云栖社区>
  2. 博客>
  3. 正文

SSO单点登录系列1:cas客户端源码分析cas-client-java-2.1.1.jar

落雨_ 2013-04-23 15:29:00 浏览579
展开阅读全文

本来没打算啃源码的,但是网上实在是没人研究,就拿出来金山词霸,跟着代码的思路一起读读吧。


希望能给以后来研究cas的兄弟留下一点思路,也算是研究了两天的成果,外国人的代码写的很晦涩,翻译下来也没有时间继续跟进,所以有错误的还原大家跟帖和我讨论,qq 394263788


edu.yale.its.tp.cas.client.filter源码分析:

/*  Copyright (c) 2000-2004 Yale University. All rights reserved.  *  See full notice at end. */  package edu.yale.its.tp.cas.client.filter;  import java.io.*;import java.net.*;import java.util.*;import javax.servlet.*;import javax.servlet.http.*;import edu.yale.its.tp.cas.client.*;  import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;  /** * <p> * Protects web-accessible resources with CAS. * </p> *  * <p> * The following filter initialization parameters are declared in <code>web.xml</code>: * </p> *  * <ul> * <li><code>edu.yale.its.tp.cas.client.filter.loginUrl</code>: URL to login page on CAS server. (Required)</li> * <li><code>edu.yale.its.tp.cas.client.filter.validateUrl</code>: URL to validation URL on CAS server. (Required)</li> * <li><code>edu.yale.its.tp.cas.client.filter.serviceUrl</code>: URL of this service. (Required if <code>serverName</code> is not specified)</li> * <li><code>edu.yale.its.tp.cas.client.filter.serverName</code>: full hostname with port number (e.g. <code>www.foo.com:8080</code>). Port number isn't required if it is standard (80 for HTTP, 443 for HTTPS). (Required if <code>serviceUrl</code> is not specified)</li> * <li><code>edu.yale.its.tp.cas.client.filter.authorizedProxy</code>: whitespace-delimited list of valid proxies through which authentication may have proceeded. One one proxy must match. (Optional. If nothing is specified, the filter will only accept service tickets &#150; not proxy tickets.)</li> * <li><code>edu.yale.its.tp.cas.client.filter.proxyCallbackUrl</code>: URL of local proxy callback listener used to acquire PGT/PGTIOU. (Optional.)</li> * <li><code>edu.yale.its.tp.cas.client.filter.renew</code>: value of CAS "renew" parameter. Bypasses single sign-on and requires user to provide CAS with his/her credentials again. (Optional. If nothing is specified, this defaults to false.)</li> * <li><code>edu.yale.its.tp.cas.client.filter.gateway</code>: value of CAS "gateway" parameter. Redirects initial call through CAS and if the user has logged in, validates the ticket on return. If the user has not logged in, returns to the web application without setting the <code>CAS_FILTER_USER</code> variable. Note that once a redirect through CAS has occurred, the filter will not automatically try again to log the user in. You can then either provide an explicit CAS login link (<code>https://cas-server/cas/login?service=http://your-app</code>) or set up two instances of the filter mapped to different paths. One instance would have gateway=true, the other wouldn't. When you need the user to be logged in, direct him/her to the path of the other filter.</li> * <li><code>edu.yale.its.tp.cas.client.filter.wrapRequest</code>: wrap the <code>HttpServletRequest</code> object, overriding the <code>getRemoteUser()</code> method. When set to "true", <code>request.getRemoteUser()</code> will return the username of the currently logged-in CAS user. (Optional. If nothing is specified, this defaults to false.)</li> * </ul> *  * <p> * The logged-in username is set in the session attribute defined by the value of <code>CAS_FILTER_USER</code> and may be accessed from within your application either by setting <code>wrapRequest</code> and calling <code>request.getRemoteUser()</code>, or by calling <code>session.getAttribute(CASFilter.CAS_FILTER_USER)</code>. * </p> *  * <p> * If <code>proxyCallbackUrl</code> is set, the URL will be passed to CAS upon validation. If the callback URL is valid, it will receive a CAS PGT and a PGTIOU. The PGTIOU will be returned to this filter and will be accessible through the session attribute, <code>CASFilter.CAS_FILTER_PGTIOU</code>. You may then acquire proxy tickets to other services by calling <code>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor.getProxyTicket(pgtIou, targetService)</code>. *  * @author Shawn Bayern * @author Drew Mazurek * @author andrew.petro@yale.edu */public class CASFilter implements Filter {      private static Log log = LogFactory.getLog(CASFilter.class);      // Filter initialization parameters    //必须参数    /**     * The name of the filter initialization parameter the value of which should be the https: address of the CAS Login servlet. Optional parameter, but required for successful redirection of unauthenticated requests to authentication.     * loginUrl:指定 CAS 提供登录页面的 URL     */    public final static String LOGIN_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.loginUrl";      /**     * The name of the filter initialization parameter the value of which must be the https: address of the CAS Validate servlet. Must be a CAS 2.0 validate servlet (CAS 1.0 non-XML won't suffice). Required parameter.     * validateUrl:指定 CAS 提供 service ticket 或 proxy ticket 验证服务的 URL      */    public final static String VALIDATE_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.validateUrl";      /**     * The name of the filter initialization parameter the value of which must be the address of the service this filter is filtering. The filter will use this as the service parameter for CAS login and validation. Either this parameter or SERVERNAME_INIT_PARAM must be set.     * serviceUrl:本web项目的URL,该参数指定过后将覆盖 serverName 参数,成为登录成功过后重定向的目的地址      */    public final static String SERVICE_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.serviceUrl";      /**     * The name of the filter initialization parameter the vlaue of which must be the server name, e.g. www.yale.edu , of the service this filter is filtering. The filter will construct from this name and the request the full service parameter for CAS login and validation.     * serverName:全主机端口号,指定客户端的域名和端口,是指客户端应用所在机器而不是 CAS Server 所在机器,该参数或 serviceUrl 至少有一个必须指定     */    public final static String SERVERNAME_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.serverName";      //可选参数    /**     * The name of the filter initialization parameter the value of which must be the String that should be sent as the "renew" parameter on the request for login and validation. This should either be "true" or not be set. It is mutually exclusive with GATEWAY.     * renew:如果指定为 true,那么受保护的资源每次被访问时均要求用户重新进行验证,而不管之前是否已经通过      */    public final static String RENEW_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.renew";      /**     * The name of the filter initialization parameter the value of which must be a whitespace delimited list of services (ProxyTicketReceptors) authorized to proxy authentication to the service filtered by this Filter. These must be https: URLs. This parameter is optional - not setting it results in no proxy tickets being acceptable.     * authorizedProxy:用于允许当前应用从代理处获取 proxy tickets,该参数接受以空格分隔开的多个 proxy URLs,但实际使用只需要一个成功即可。当指定该参数过后,需要修改 validateUrl 到 proxyValidate,     */    public final static String AUTHORIZED_PROXY_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.authorizedProxy";      /**     * The name of the filter initialization parameter the value of which must be the https: URL to which CAS should send Proxy Granting Tickets when this filter validates tickets.     * proxyCallbackUrl:用于当前应用需要作为其他服务的代理(proxy)时获取 Proxy Granting Ticket 的地址      */    public final static String PROXY_CALLBACK_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.proxyCallbackUrl";      /**     * The name of the filter initialization parameter the value of which indicates whether this filter should wrap requests to expose the authenticated username.     * wrapRequest:如果指定为 true,那么 CASFilter 将重新包装 HttpRequest,并且使 getRemoteUser() 方法返回当前登录用户的用户名     */    public final static String WRAP_REQUESTS_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.wrapRequest";      /**     * The name of the filter initialization parameter the value of which is the value the Filter should send for the gateway parameter on the CAS login request.     * gateway:这个参数很奇葩,一开始没读懂是干嘛的。。官方解释是一旦发生过CAS重定向,过滤器将不会自动重新设置登录的用户。然后你可以提供一个明确的CAS登录链接(HTTPS:/ / CAS服务器/ CAS /登录?服务= HTTP:/ /应用程序)或建立映射到不同的路径的过滤器的两个实例。一个实例将gateway实现。当你需要登录的用户,直接转到其他过滤器。     * 是的你没有想错,这一句话着实让人不知道是要说明什么,于是万能的百度上有且仅有一个前辈说出来了这个参数其实是和renew互斥的,renew就是说无论如何都得重新验证此用户,不管你session中有没有上下文信息。而gateway则是只要检测到session中有sso上下文,就不再重新认证     */    public final static String GATEWAY_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.gateway";      // Session attributes used by this filter      /**     * <p>     * Session attribute in which the username is stored.     * </p>     */    public final static String CAS_FILTER_USER = "edu.yale.its.tp.cas.client.filter.user";      /**     * Session attribute in which the CASReceipt is stored.     */    public final static String CAS_FILTER_RECEIPT = "edu.yale.its.tp.cas.client.filter.receipt";      /**     * Session attribute in which internally used gateway attribute is stored.     */    private static final String CAS_FILTER_GATEWAYED = "edu.yale.its.tp.cas.client.filter.didGateway";      // *********************************************************************    // Configuration state      /** Secure URL whereat CAS offers its login service. */    private String casLogin;    /** Secure URL whereat CAS offers its CAS 2.0 validate service */    private String casValidate;    /** Filtered service URL for use as service parameter to login and validate */    private String casServiceUrl;    /** Name of server, for use in assembling service URL for use as service parameter to login and validate. */    private String casServerName;    /** Secure URL whereto this filter should ask CAS to send Proxy Granting Tickets. */    private String casProxyCallbackUrl;      /** True if renew parameter should be set on login and validate */    private boolean casRenew;      /** True if this filter should wrap requests to expose authenticated user as getRemoteUser(); */    private boolean wrapRequest;      /** True if this filter should set gateway=true on login redirect */    private boolean casGateway = false;      /**     * List of ProxyTicketReceptor URLs of services authorized to proxy to the path behind this filter.     * 对proxyticketreceptor URL授权代理在过滤器的路径的服务列表     */    private List authorizedProxies = new ArrayList();      // *********************************************************************    // Initialization      public void init(FilterConfig config) throws ServletException {        //拿到参数        casLogin = config.getInitParameter(LOGIN_INIT_PARAM);        casValidate = config.getInitParameter(VALIDATE_INIT_PARAM);        casServiceUrl = config.getInitParameter(SERVICE_INIT_PARAM);        String casAuthorizedProxy = config.getInitParameter(AUTHORIZED_PROXY_INIT_PARAM);        casRenew = Boolean.valueOf(config.getInitParameter(RENEW_INIT_PARAM)).booleanValue();        casServerName = config.getInitParameter(SERVERNAME_INIT_PARAM);        casProxyCallbackUrl = config.getInitParameter(PROXY_CALLBACK_INIT_PARAM);        wrapRequest = Boolean.valueOf(config.getInitParameter(WRAP_REQUESTS_INIT_PARAM)).booleanValue();        casGateway = Boolean.valueOf(config.getInitParameter(GATEWAY_INIT_PARAM)).booleanValue();          if (casGateway && Boolean.valueOf(casRenew).booleanValue()) {            //这俩参数不能一起设置为true            throw new ServletException("gateway and renew cannot both be true in filter configuration");        }        if (casServerName != null && casServiceUrl != null) {            //这俩参数也不能一起设置            throw new ServletException("serverName and serviceUrl cannot both be set: choose one.");        }        if (casServerName == null && casServiceUrl == null) {            //这俩参数也不能一起为null            throw new ServletException("one of serverName or serviceUrl must be set.");        }        if (casServiceUrl != null) {            //检测uri前缀            if (!(casServiceUrl.startsWith("https://") || (casServiceUrl.startsWith("http://")))) {                throw new ServletException("service URL must start with http:// or https://; its current value is [" + casServiceUrl + "]");            }        }          if (casValidate == null) {            //cas验证用户的网址不能为空            throw new ServletException("validateUrl parameter must be set.");        }        if (!casValidate.startsWith("https://")) {            //如果cas认证网址不是以https开头,就报错。。如果你是用http请求,可以屏蔽掉这个判断语句            throw new ServletException("validateUrl must start with https://, its current value is [" + casValidate + "]");        }        //代理是否为空        if (casAuthorizedProxy != null) {              // parse and remember authorized proxies            StringTokenizer casProxies = new StringTokenizer(casAuthorizedProxy);            while (casProxies.hasMoreTokens()) {                //授权的标记                String anAuthorizedProxy = casProxies.nextToken();                //https前缀检测                if (!anAuthorizedProxy.startsWith("https://")) {                    throw new ServletException("CASFilter initialization parameter for authorized proxies " + "must be a whitespace delimited list of authorized proxies.  " + "Authorized proxies must be secure (https) addresses.  This one wasn't: [" + anAuthorizedProxy + "]");                }                //将所有授权的代理添加到list中(唉,着实不知道是干什么的,也许几年后回来读读应该能知道答案,2013年4月22日14:56:37)                this.authorizedProxies.add(anAuthorizedProxy);            }        }          if (log.isDebugEnabled()) {            log.debug(("CASFilter initialized as: [" + toString() + "]"));        }    }      // *********************************************************************    // Filter processing    // 过滤器处理    public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws ServletException, IOException {                  //核心思想:首先检查session中有无凭证receipt,如果有,那么就要去下个过滤器链进行处理,如果无,则获取传参ticket,如果有ticket,就经过getAuthenticatedUser()方法去拿到receipt凭证,如果无(这中间会有一些对renew或者gateway的处理),就立即进入cas服务端进行登录        if (log.isTraceEnabled()) {            log.trace("entering doFilter()");        }          // make sure we've got an HTTP request        if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {            log.error("doFilter() called on a request or response that was not an HttpServletRequest or response.");            throw new ServletException("CASFilter protects only HTTP resources");        }          // Is this a request for the proxy callback listener? If so, pass        // it through        if (casProxyCallbackUrl != null && casProxyCallbackUrl.endsWith(((HttpServletRequest) request).getRequestURI()) && request.getParameter("pgtId") != null && request.getParameter("pgtIou") != null) {            log.trace("passing through what we hope is CAS's request for proxy ticket receptor.");            fc.doFilter(request, response);            return;        }          // Wrap the request if desired        if (wrapRequest) {            log.trace("Wrapping request with CASFilterRequestWrapper.");            request = new CASFilterRequestWrapper((HttpServletRequest) request);        }        // 1.从当前web应用中拿到session        HttpSession session = ((HttpServletRequest) request).getSession();          // if our attribute's already present and valid, pass through the filter chain          // 1.1.如果存在一个票据(令牌,凭证),就要跳到下一个过滤器链(去验证此票据的真实性,因为此票据的真实性是未知的)        CASReceipt receipt = (CASReceipt) session.getAttribute(CAS_FILTER_RECEIPT);        if (receipt != null && isReceiptAcceptable(receipt)) {            log.trace("CAS_FILTER_RECEIPT attribute was present and acceptable - passing  request through filter..");            fc.doFilter(request, response);            return;        }               // otherwise, we need to authenticate via CAS        // 1.2.如果receipt(令牌)不存在就先拿到ticket,我们要去cas验证用户进行登录        String ticket = request.getParameter("ticket");        // no ticket? abort request processing and redirect        //如果ticket为空        if (ticket == null || ticket.equals("")) {            log.trace("CAS ticket was not present on request.");              // 4.1判断是否经过网关参数(didGateway这个参数否已经经过网关的一个标记参数,表示不再进行认证)            // did we go through the gateway already?            boolean didGateway = Boolean.valueOf((String) session.getAttribute(CAS_FILTER_GATEWAYED)).booleanValue();            // 4.1.1没有casLogin的配置信息下的异常处理            if (casLogin == null) {                // TODO: casLogin should probably be ensured to not be null at filter initialization. -awp9                log.fatal("casLogin was not set, so filter cannot redirect request for authentication.");                throw new ServletException("When CASFilter protects pages that do not receive a 'ticket' " + "parameter, it needs a edu.yale.its.tp.cas.client.filter.loginUrl " + "filter parameter");            }            // 4.2如果网关标记为false,设置CAS_FILTER_GATEWAYED属性为true,并跳转到cas服务端进行验证            if (!didGateway) {                log.trace("Did not previously gateway.  Setting session attribute to true.");                session.setAttribute(CAS_FILTER_GATEWAYED, "true");                redirectToCAS((HttpServletRequest) request, (HttpServletResponse) response);                // abort chain                return;            } else {                log.trace("Previously gatewayed.");                // 4.3 如果有网关参数(之前已经通过了网关),就不再进行验证,从而进入下一个过滤器处理即可。                // if we should be logged in, make sure validation succeeded                if (casGateway || session.getAttribute(CAS_FILTER_USER) != null) {                    //已经通过了验证和授权。。                    log.trace("casGateway was true and CAS_FILTER_USER set: passing request along filter chain.");                    // continue processing the request 交给下一个过滤器                    fc.doFilter(request, response);                    return;                } else {                    // 其他情况下,跳往cas服务端                    // unknown state... redirect to CAS                    //将经过网关的参数didGateway设置为true                    session.setAttribute(CAS_FILTER_GATEWAYED, "true");                    redirectToCAS((HttpServletRequest) request, (HttpServletResponse) response);                                          // abort chain                    return;                }            }        }              try {            // ticket存在,就经过getAuthenticatedUser()方法去拿到receipt,初步判断此方法是为根据request中的ticket参数组装了一个数据发送给了cas服务端进行判断此ticket是否是正确的合法的(它可能是使用代理类进行的实现)            receipt = getAuthenticatedUser((HttpServletRequest) request);        } catch (CASAuthenticationException e) {            log.error(e);            throw new ServletException(e);        }          if (!isReceiptAcceptable(receipt)) {            //检测授权不被认可,就是非法的。            throw new ServletException("Authentication was technically successful but rejected as a matter of policy. [" + receipt + "]");        }        //既然拿到了凭证,就去拿到session中是否有相关信息,并写入CASFilter.CAS_FILTER_RECEIPT        // Store the authenticated user in the session        if (session != null) { // probably unnecessary            //将username(用户名)信息放入session中            session.setAttribute(CAS_FILTER_USER, receipt.getUserName());            //放入票据            session.setAttribute(CASFilter.CAS_FILTER_RECEIPT, receipt);            // don't store extra unnecessary session state            //不要储存额外的不必要的会话状态            session.removeAttribute(CAS_FILTER_GATEWAYED);        }        if (log.isTraceEnabled()) {            log.trace("validated ticket to get authenticated receipt [" + receipt + "], now passing request along filter chain.");        }          // continue processing the request        //进入下一个过滤器进行处理        fc.doFilter(request, response);        log.trace("returning from doFilter()");    }      /**     * Is this receipt acceptable as evidence of authentication by credentials that would have been acceptable to this path? Current implementation checks whether from renew and whether proxy was authorized.     *      * @param receipt 票据     * @return true if acceptable, false otherwise     */    private boolean isReceiptAcceptable(CASReceipt receipt) {        if (receipt == null)            throw new IllegalArgumentException("Cannot evaluate a null receipt.");        if (this.casRenew && !receipt.isPrimaryAuthentication()) {            return false;        }        if (receipt.isProxied()) {            if (!this.authorizedProxies.contains(receipt.getProxyingService())) {                return false;            }        }        return true;    }      // *********************************************************************    // Utility methods      /**     * Converts a ticket parameter to a CASReceipt, taking into account an optionally configured trusted proxy in the tier immediately in front of us.     *      * @throws ServletException -     *             when unable to get service for request     * @throws CASAuthenticationException -     *             on authentication failure     */    private CASReceipt getAuthenticatedUser(HttpServletRequest request) throws ServletException, CASAuthenticationException {        log.trace("entering getAuthenticatedUser()");        ProxyTicketValidator pv = null;          pv = new ProxyTicketValidator();        pv.setCasValidateUrl(casValidate);        pv.setServiceTicket(request.getParameter("ticket"));        pv.setService(getService(request));        pv.setRenew(Boolean.valueOf(casRenew).booleanValue());        if (casProxyCallbackUrl != null) {            pv.setProxyCallbackUrl(casProxyCallbackUrl);        }        if (log.isDebugEnabled()) {            log.debug("about to validate ProxyTicketValidator: [" + pv + "]");        }          return CASReceipt.getReceipt(pv);      }      /**     * Returns either the configured service or figures it out for the current request. The returned service is URL-encoded.     */    private String getService(HttpServletRequest request) throws ServletException {          log.trace("entering getService()");        String serviceString;          // ensure we have a server name or service name        if (casServerName == null && casServiceUrl == null)            throw new ServletException("need one of the following configuration " + "parameters: edu.yale.its.tp.cas.client.filter.serviceUrl or " + "edu.yale.its.tp.cas.client.filter.serverName");          // use the given string if it's provided        if (casServiceUrl != null)            serviceString = URLEncoder.encode(casServiceUrl);        else            // otherwise, return our best guess at the service            serviceString = Util.getService(request, casServerName);        if (log.isTraceEnabled()) {            log.trace("returning from getService() with service [" + serviceString + "]");        }        return serviceString;    }      /**     * Redirects the user to CAS, determining the service from the request.     */    private void redirectToCAS(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {        if (log.isTraceEnabled()) {            log.trace("entering redirectToCAS()");        }          String casLoginString = casLogin + "?service=" + getService((HttpServletRequest) request) + ((casRenew) ? "&renew=true" : "") + (casGateway ? "&gateway=true" : "");          if (log.isDebugEnabled()) {            log.debug("Redirecting browser to [" + casLoginString + ")");        }        ((HttpServletResponse) response).sendRedirect(casLoginString);          if (log.isTraceEnabled()) {            log.trace("returning from redirectToCAS()");        }    }      public String toString() {        StringBuffer sb = new StringBuffer();   

网友评论

登录后评论
0/500
评论
落雨_
+ 关注