CAS client 源码分析

作者: 巨子联盟 | 来源:发表于2018-05-17 09:20 被阅读0次
    AbstractConfigurationFilter.png
    从上图可以看到,CAS有很多Filter,主要包括认证Filter和验证Ticket的Filter,还有一些上图没有的,比如:AssertionThreadLocalFilterDelegatingFilter,下面一一分析下.
    • AbstractConfigurationFilter:

    这个类是很多关键Filter的父类,提供了基础的配置外置化功能.其实我们也可以在自己的项目里直接继承它.

    1. 有一个属性:ConfigurationStrategy configurationStrategy,
      配置Filter时,提供一个配置属性的策略.方便外部控制Filter的属性值
      主要代码:
        public void init(FilterConfig filterConfig) throws ServletException {
            final String configurationStrategyName = filterConfig.getServletContext().getInitParameter(CONFIGURATION_STRATEGY_KEY);
            //ConfigurationStrategyName 是一个枚举类,里面设定了几个client实现了的onfigurationStrategy,默认是LegacyConfigurationStrategyImpl
            //ConfigurationStrategyName.resolveToConfigurationStrategy 处理了枚举类定义的几个实现,
            //外部 configurationStrategyName="PROPERTY_FILE"就可以了
            //同时也支持类完整名称初始化
            this.configurationStrategy = ReflectUtils.newInstance(ConfigurationStrategyName.resolveToConfigurationStrategy(configurationStrategyName));
            this.configurationStrategy.init(filterConfig, getClass());
        }
    
    1. 提供了一些方便获取配置的方法,如:getBoolean(final ConfigurationKey<Boolean> configurationKey)

    • ConfigurationStrategy:这个接口定义了init方法和方便获取配置值的方法,有5个实现类
      BaseConfigurationStrategy 中处理了方便获取配置的方法.getBoolean,getLong,getInt,getClass
      实现获取配置的方法用了我们常用的回调,java8可改成lambda形式,在此略过...
    1. LegacyConfigurationStrategyImpl在用默认值之前会先用WebXmlConfigurationStrategyImpl再用JndiConfigurationStrategyImpl方式获取,
      如果没有才用 ConfigurationKey 的默认值

    2. WebXmlConfigurationStrategyImpl 先调用 filterConfig.getInitParameter 方法获取再调用 filterConfig.getServletContext().getInitParameter 获取

    3. JndiConfigurationStrategyImpl 从JNDI中获取配置,ENVIRONMENT_PREFIX = "java:comp/env/cas/";

    4. PropertiesConfigurationStrategyImpl 先通过 filterConfig.getInitParameter 获取configFileLocation参数的文件位置,
      没有再用 filterConfig.getServletContext().getInitParameter 获取文件,
      再没有就从默认的位置DEFAULT_CONFIGURATION_FILE_LOCATION = /etc/java-cas-client.properties获取配置,还没有就报错啦...

    5. SystemPropertiesConfigurationStrategyImpl 从系统环境参数中获取 System.getProperty
      这几个类的类图如下:

      ConfigurationStrategy.png

    • HttpServletRequestWrapperFilter
    1. 封装 HttpServletRequest 添加 getUserPrincipal(),getRemoteUser(),isUserInRole(String) 方法
    2. 里面有个关键类 CasHttpServletRequestWrapper 继承自 HttpServletRequestWrapper 并在里面添加了上面3个方法,就是这个包装下request,这样就多了几个方法,
      getUserPrincipal() 从request或session中获取信息,CONST_CAS_ASSERTION = "_const_cas_assertion_"
      isUserInRole(final String role) 判断是否为指定的role,关键代码this.principal.getAttributes().get(roleAttribute),从principal获取属性
      roleAttribute 这个值可以是在配置中指定getString(ConfigurationKeys.ROLE_ATTRIBUTE);
      也可以配置是否忽略大小写getBoolean(ConfigurationKeys.IGNORE_CASE),这样比较role的时候就会忽略大小写
    • SingleSignOutFilter

    管理登出
    这里面的逻辑都委托给了类 SingleSignOutHandler 处理,包括 init和process
    这个类比较有意思的是对init的处理,代码中的注释说,因为受spring security的影响,在调用doFilter方法的时候有可能init方法还没调用,这时SingleSignOutHandler还没初始化好,
    所以在doFilter方法里面又初始化了一次.代码如下:

        //定义了个线程安全的属性,后面用 getAndSet方法保证原子操作
         private AtomicBoolean handlerInitialized = new AtomicBoolean(false);
         ...
         //在doFilter方法调用
            if (!this.handlerInitialized.getAndSet(true)) {
                HANDLER.init();
            }
    

    接着我们分析下 SingleSignOutHandler
    这个类是真正干活的,里面有很多属性,比较有意思的是: SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage()
    LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy()
    初始化方法也是对这些变量赋值和校验的过程
    真正的逻辑处理在process,代码贴出来:

        public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
            //artifactParameterName 默认等于 ticket ,logoutParameterName 默认等于 logoutRequest
            //判断是否为带了ticket参数的请求
            if (isTokenRequest(request)) {
                logger.trace("Received a token request");
                //调用 SessionMappingStorage 创建 session,默认是 HashMapBackedSessionMappingStorage
                //这个存储方式就是简单的把session和ticket 存储起来
                //ID_TO_SESSION_KEY_MAPPING 是以session id为key,ticket为值
                //MANAGED_SESSIONS 是以 ticket为key ,HttpSession 为值
                //当用户很多的时候就要嗝屁了
                //里面用到了request.getSession(this.eagerlyCreateSessions),关于获取session的这个eagerlyCreateSessions参数,我们如果不要session时要将其设置为false,这个值默认为true
    //request.getSession(true/false/null)的区别
    //HttpServletRequest.getSession(ture)等同于 HttpServletRequest.getSession() 
    //HttpServletRequest.getSession(false)等同于 如果当前Session没有就为null; 
    //当向Session中存取登录信息时,一般建议:HttpSession session =request.getSession();
    //当从Session中获取登录信息时,一般建议:HttpSession session =request.getSession(false);
                recordSession(request);
                return true;
    
            }
            //是否为登出请求,处理了GET和POST,附件的类型
            if (isLogoutRequest(request)) {
                logger.trace("Received a logout request");
                //从请求中获取logoutRequest 参数信息,解压得到sessionId,然后调用 session.invalidate()
                //同时也清除 sessionMappingStorage 中的信息
                destroySession(request);
                return false;
            } 
            logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
            return true;        
        }
    

    • AbstractCasFilter

    这个类抽象出了CAS很多filter都要用的方法,比如重要的方法如下:

        protected final String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
            return CommonUtils.constructServiceUrl(request, response, this.service, this.serverName,
                    this.protocol.getServiceParameterName(),
                    this.protocol.getArtifactParameterName(), this.encodeServiceUrl);
        }
    

    分析下 CommonUtils.constructServiceUrl

        /**
         * Constructs a service url from the HttpServletRequest or from the given
         * serviceUrl. Prefers the serviceUrl provided if both a serviceUrl and a
         * serviceName.
         *
         * @param request the HttpServletRequest
         * @param response the HttpServletResponse
         * @param service the configured service url (this will be used if not null)
         * @param serverNames the server name to  use to construct the service url if the service param is empty.  Note, prior to CAS Client 3.3, this was a single value.
         *           As of 3.3, it can be a space-separated value.  We keep it as a single value, but will convert it to an array internally to get the matching value. This keeps backward compatability with anything using this public
         *           method.
         * @param serviceParameterName the service parameter name to remove (i.e. service)
         * @param artifactParameterName the artifact parameter name to remove (i.e. ticket)
         * @param encode whether to encode the url or not (i.e. Jsession).
         * @return the service url to use.
         */
        public static String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response,
                final String service, final String serverNames, final String serviceParameterName,
                final String artifactParameterName, final boolean encode) {
            if (CommonUtils.isNotBlank(service)) {
                return encode ? response.encodeURL(service) : service;
            }
            //会从 Host 和 X-Forwarded-Host 请求中获取server name
            final String serverName = findMatchingServerName(request, serverNames);
            final URIBuilder originalRequestUrl = new URIBuilder(request.getRequestURL().toString(), encode);
            originalRequestUrl.setParameters(request.getQueryString());
            //CAS 工具箱中的 builder
            final URIBuilder builder;
            
            boolean containsScheme = true;
            if (!serverName.startsWith("https://") && !serverName.startsWith("http://")) {
                builder = new URIBuilder(encode);
                builder.setScheme(request.isSecure() ? "https" : "http");
                builder.setHost(serverName);
                containsScheme = false;
            }  else {
                builder = new URIBuilder(serverName, encode);
            }
    
            //判断是否包含端口或者是标准端口,此处被坑过
            if (!serverNameContainsPort(containsScheme, serverName) && !requestIsOnStandardPort(request)) {
                builder.setPort(request.getServerPort());
            }
    
            builder.setEncodedPath(request.getRequestURI());
    
            final List<String> serviceParameterNames = Arrays.asList(serviceParameterName.split(","));
            if (!serviceParameterNames.isEmpty() && !originalRequestUrl.getQueryParams().isEmpty()) {
                for (final URIBuilder.BasicNameValuePair pair : originalRequestUrl.getQueryParams()) {
                    String name = pair.getName();
                    if (!name.equals(artifactParameterName) && !serviceParameterNames.contains(name)) {
                        if (name.contains("&") || name.contains("=") ){
                            URIBuilder encodedParamBuilder = new URIBuilder();
                            encodedParamBuilder.setParameters(name);
                            for (final URIBuilder.BasicNameValuePair pair2 :encodedParamBuilder.getQueryParams()){
                                String name2 = pair2.getName();
                                if (!name2.equals(artifactParameterName) && !serviceParameterNames.contains(name2)) {
                                    builder.addParameter(name2, pair2.getValue());
                                }
                            }
                        } else {
                            builder.addParameter(name, pair.getValue());
                        }
                    }
                }
            }
    
            final String result = builder.toString();
            final String returnValue = encode ? response.encodeURL(result) : result;
            LOGGER.debug("serviceUrl generated: {}", returnValue);
            return returnValue;
        }
    
    • AuthenticationFilter

    认证过滤器
    这个类主要有两个方法实现逻辑,initInternaldoFilter
    先分析怎么初始化的

        protected void initInternal(final FilterConfig filterConfig) throws ServletException {
            //同样的套路,处理初始化开关
            if (!isIgnoreInitConfiguration()) {
                //先调用父类的初始化
                super.initInternal(filterConfig);
                setCasServerLoginUrl(getString(ConfigurationKeys.CAS_SERVER_LOGIN_URL));
                setRenew(getBoolean(ConfigurationKeys.RENEW));
                setGateway(getBoolean(ConfigurationKeys.GATEWAY));
                
                //下面这段逻辑都是处理忽略认证的逻辑的
                //先从static里面初始化的几种系统预设 PATTERN_MATCHER_TYPES 里面取,值分别是:CONTAINS,REGEX(默认值),EXACT
                //没有匹配到再从Filter配置里面获取,参数值为 ignoreUrlPatternType
                //如果还没有,那就是为null了,后期在 isRequestUrlExcluded 方法里判断的时候直接返回false,也就是所有的都要校验
                //isRequestUrlExcluded 方法校验的时候会加上 request.getQueryString() 参数值一起匹配
                final String ignorePattern = getString(ConfigurationKeys.IGNORE_PATTERN);
                final String ignoreUrlPatternType = getString(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE);
                
                if (ignorePattern != null) {
                    final Class<? extends UrlPatternMatcherStrategy> ignoreUrlMatcherClass = PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);
                    if (ignoreUrlMatcherClass != null) {
                        this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlMatcherClass.getName());
                    } else {
                        try {
                            logger.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType);
                            this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlPatternType);
                        } catch (final IllegalArgumentException e) {
                            logger.error("Could not instantiate class [{}]", ignoreUrlPatternType, e);
                        }
                    }
                    if (this.ignoreUrlPatternMatcherStrategyClass != null) {
                        this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);
                    }
                }
                //GatewayResolver是什么用的?
                //有两个方法,hasGatewayedAlready 从session中获取_const_cas_gateway_标志,判断是否已经存在; storeGatewayInformation是存入session信息
                //默认值为:DefaultGatewayResolverImpl,
                final Class<? extends GatewayResolver> gatewayStorageClass = getClass(ConfigurationKeys.GATEWAY_STORAGE_CLASS);
    
                if (gatewayStorageClass != null) {
                    setGatewayStorage(ReflectUtils.newInstance(gatewayStorageClass));
                }
                // 设置认证后的跳转策略,默认为:DefaultAuthenticationRedirectStrategy,就一句话 response.sendRedirect(potentialRedirectUrl);
                final Class<? extends AuthenticationRedirectStrategy> authenticationRedirectStrategyClass = getClass(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS);
    
                if (authenticationRedirectStrategyClass != null) {
                    this.authenticationRedirectStrategy = ReflectUtils.newInstance(authenticationRedirectStrategyClass);
                }
            }
        }
    

    再来分析过滤方法 doFilter

        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;
            //调用上面的策略过滤是否需要认证
            if (isRequestUrlExcluded(request)) {
                logger.debug("Request is ignored.");
                filterChain.doFilter(request, response);
                return;
            }
            //获取session,没有就返回null,不创建
            final HttpSession session = request.getSession(false);
            //CONST_CAS_ASSERTION = _const_cas_assertion_
            final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
            //已经认证过的不用再认证了
            if (assertion != null) {
                filterChain.doFilter(request, response);
                return;
            }
            //组装serviceURL
            final String serviceUrl = constructServiceUrl(request, response);
            final String ticket = retrieveTicketFromRequest(request);
            final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
            //关于Gateway的用法资料
            //https://wiki.jasig.org/display/casc/cas+java+client+gateway+example
            //http://exceptioneye.iteye.com/blog/1889278
            if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
                filterChain.doFilter(request, response);
                return;
            }
    
            final String modifiedServiceUrl;
    
            logger.debug("no ticket and no assertion found");
            if (this.gateway) {
                logger.debug("setting gateway attribute in session");
                //标记已经Gateway了,后面只有ticket参数就可以直接通过了,不用调登录了
                modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
            } else {
                modifiedServiceUrl = serviceUrl;
            }
    
            logger.debug("Constructed service url: {}", modifiedServiceUrl);
    
            final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
                    getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
    
            logger.debug("redirecting to \"{}\"", urlToRedirectTo);
            this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
        }
    
    • AbstractTicketValidationFilter

    处理认证ticket的一切
    getSSLConfig 获取SSL的配置信息
    getHostnameVerifier 获取 HostnameVerifier 的配置信息
    这两个方法都是处理HTTPS的一些配置

        protected void initInternal(final FilterConfig filterConfig) throws ServletException {
            setExceptionOnValidationFailure(getBoolean(ConfigurationKeys.EXCEPTION_ON_VALIDATION_FAILURE));
            setRedirectAfterValidation(getBoolean(ConfigurationKeys.REDIRECT_AFTER_VALIDATION));
            setUseSession(getBoolean(ConfigurationKeys.USE_SESSION));
            //为了避免无限重定向,当不用session时会强制设置 RedirectAfterValidation = false
            if (!this.useSession && this.redirectAfterValidation) {
                logger.warn("redirectAfterValidation parameter may not be true when useSession parameter is false. Resetting it to false in order to prevent infinite redirects.");
                setRedirectAfterValidation(false);
            }
            //
            setTicketValidator(getTicketValidator(filterConfig));
            super.initInternal(filterConfig);
        }
    

    上面的初始化方法很简单.略...
    下面分析doFilter方法

        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 = retrieveTicketFromRequest(request);
            //只处理有ticket参数的请求
            if (CommonUtils.isNotBlank(ticket)) {
                logger.debug("Attempting to validate ticket: {}", ticket);
    
                try {
                    //调用认证器认证请求
                    final Assertion assertion = this.ticketValidator.validate(ticket,
                            constructServiceUrl(request, response));
    
                    logger.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) {
                        logger.debug("Redirecting after successful ticket validation.");
                        response.sendRedirect(constructServiceUrl(request, response));
                        return;
                    }
                } catch (final TicketValidationException e) {
                    logger.debug(e.getMessage(), e);
    
                    onFailedValidation(request, response);
    
                    if (this.exceptionOnValidationFailure) {
                        throw new ServletException(e);
                    }
    
                    response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
    
                    return;
                }
            }
    
            filterChain.doFilter(request, response);
    
        }
    


    从上面代码可以看到,关键就是ticketValidator 认证器了,下面详细分析
    首先贴类图:


    TicketValidator.png
    • AbstractUrlBasedTicketValidator

    根据名字和类图的位置很显然这个是个抽象基类,处理一些公共的事情,在这里用到了模板方法,定义了整个验证处理的流程,真正的逻辑在具体的类中

        public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
            //组装成验证的请求,这个方法里面也定义了一写抽象方法
            final String validationUrl = constructValidationUrl(ticket, service);
            logger.debug("Constructing validation url: {}", validationUrl);
    
            try {
                logger.debug("Retrieving response from server.");
                //连接CAS Server验证ticket,这里定义了一个抽象方法
                final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
    
                if (serverResponse == null) {
                    throw new TicketValidationException("The CAS server returned no response.");
                }
    
                logger.debug("Server response: {}", serverResponse);
                //将CAS server的响应信息封装成 Assertion,这里也是定义了一个抽象方法
                return parseResponseFromServer(serverResponse);
            } catch (final MalformedURLException e) {
                throw new TicketValidationException(e);
            }
        }
    
    • AbstractCasProtocolUrlBasedTicketValidator

    实现了一个方法 retrieveResponseFromServer
    就一行代码
    return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
    我们分析下 CommonUtils.getResponseFromServer

        public static String getResponseFromServer(final URL constructedUrl, final HttpURLConnectionFactory factory,
                final String encoding) {
            
            HttpURLConnection conn = null;
            InputStreamReader in = null;
            try {
                //核心代码就这一句,factory有两个实现,这里一般是用 HttpsURLConnectionFactory
                //里面主要是对SSLSocketFactory和HostnameVerifier 两个属性进行特殊赋值,有时我们需要用到
                //HostnameVerifier在CAS client里有3中实现,AnyHostnameVerifier,RegexHostnameVerifier和WhitelistHostnameVerifier         
                conn = factory.buildHttpURLConnection(constructedUrl.openConnection());
    
                if (CommonUtils.isEmpty(encoding)) {
                    in = new InputStreamReader(conn.getInputStream());
                } else {
                    in = new InputStreamReader(conn.getInputStream(), encoding);
                }
    
                final StringBuilder builder = new StringBuilder(255);
                int byteRead;
                while ((byteRead = in.read()) != -1) {
                    builder.append((char) byteRead);
                }
    
                return builder.toString();
            } catch (final RuntimeException e) {
                throw e;
            } catch (final SSLException e) {
                LOGGER.error("SSL error getting response from host: {} : Error Message: {}", constructedUrl.getHost(), e.getMessage(), e);
                throw new RuntimeException(e);
            } catch (final IOException e) {
                LOGGER.error("Error getting response from host: [{}] with path: [{}] and protocol: [{}] Error Message: {}",
                        constructedUrl.getHost(), constructedUrl.getPath(), constructedUrl.getProtocol(), e.getMessage(), e);
                throw new RuntimeException(e);
            } finally {
                closeQuietly(in);
                if (conn != null) {
                    conn.disconnect();
                }
            }
        }
    
    • Cas10TicketValidator

    用CAS1.0协议的验证器.代码很简单,现在都基本不用了,端点是:validate

    • Cas20ServiceTicketValidator

    支持CAS2.0,端点是 serviceValidate
    有3个属性:
    proxyCallbackUrl,proxyGrantingTicketStorage 和 proxyRetriever
    里面主要也是覆写了一个方法 parseResponseFromServer

    
        protected Assertion parseResponseFromServer(final String response) throws TicketValidationException {
            //从返回的XML中获取失败信息 ,代码就一行 XmlUtils.getTextForElement(response, "authenticationFailure");
            final String error = parseAuthenticationFailureFromResponse(response);
            //失败了直接抛异常
            if (CommonUtils.isNotBlank(error)) {
                throw new TicketValidationException(error);
            }
            //获取用户认证信息 XmlUtils.getTextForElement(response, "user");
            final String principal = parsePrincipalFromResponse(response);
            //一样一行代码,XmlUtils.getTextForElement(response, "proxyGrantingTicket");
            final String proxyGrantingTicketIou = parseProxyGrantingTicketFromResponse(response);
    
            final String proxyGrantingTicket;
            if (CommonUtils.isBlank(proxyGrantingTicketIou) || this.proxyGrantingTicketStorage == null) {
                proxyGrantingTicket = null;
            } else {
                // ProxyGrantingTicketStorage 有两个实现 ProxyGrantingTicketStore 和 ProxyGrantingTicketStorageImpl
                // CasConfiguration 中用的是 ProxyGrantingTicketStore,用Google的Guava 缓存实现        
                //ProxyGrantingTicketStorageImpl 用的是 ConcurrentHashMap 实现,对于超时的处理是通过 CleanUpTimerTask 定时任务去处理的
                proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
            }
    
            if (CommonUtils.isEmpty(principal)) {
                throw new TicketValidationException("No principal was found in the response from the CAS server.");
            }
    
            final Assertion assertion;
            //调用 SAX将返回的数据转换成Map,里面定义了 CustomAttributeHandler做转换处理
            final Map<String, Object> attributes = extractCustomAttributes(response);
            if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
                final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes,
                        proxyGrantingTicket, this.proxyRetriever);
                assertion = new AssertionImpl(attributePrincipal);
            } else {
                assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
            }
            //模板方法
            customParseResponse(response, assertion);
    
            return assertion;
        }
    
    • Cas30ServiceTicketValidator

    继承自Cas20ServiceTicketValidator 只改变了端口为p3/serviceValidate

    • Cas30JsonServiceTicketValidator

    处理CAS Server返回值是JSON格式的情况

    
        @Override
        protected Assertion parseResponseFromServer(final String response) throws TicketValidationException {
            try {
                //JsonValidationResponseParser 负责将 JSON传转换成 TicketValidationJsonResponse对象,用 Jackson的ObjectMapper
                //并校验是否成功
                final TicketValidationJsonResponse json = new JsonValidationResponseParser().parse(response);
                //TicketValidationJsonResponse 负责转换成需要的 Assertion
                return json.getAssertion(getProxyGrantingTicketStorage(), getProxyRetriever());
            } catch (final JsonProcessingException e) {
                //处理JSON失败就默认用XML格式处理
                logger.warn("Unable parse the JSON response. Falling back to XML", e);
                return super.parseResponseFromServer(response);
            } catch (final IOException e) {
                throw new TicketValidationException(e.getMessage(), e);
            }
        }
    
    • Cas20ProxyTicketValidator

    继承自 Cas20ServiceTicketValidator 端点为: proxyValidate
    覆写了 customParseResponse方法,该方法是 Cas20ServiceTicketValidatorparseResponseFromServer 最后面调用的
    主要是对Proxy做了一些验证

    • Cas30ProxyTicketValidator

    端点为:p3/proxyValidate,其他的方法都没覆写

    • Cas30JsonProxyTicketValidator

    处理JSON格式

        @Override
        protected Assertion parseResponseFromServer(final String response) throws TicketValidationException {
            try {
                //和前面 Cas30JsonServiceTicketValidator 一样的套路
                final TicketValidationJsonResponse json = new JsonValidationResponseParser().parse(response);
                return json.getAssertion(getProxyGrantingTicketStorage(), getProxyRetriever());
            } catch (final Exception e) {
                logger.warn("Unable parse the JSON response");
                return super.parseResponseFromServer(response);
            }
        }
    
        //这个是 Cas20ProxyTicketValidator 中定义的方法, Cas20ProxyTicketValidator是处理XML的,这个是处理JSON的
        @Override
        protected List<String> parseProxiesFromResponse(final String response) {
            try {
                //和前面 Cas30JsonServiceTicketValidator 一样的套路
                final TicketValidationJsonResponse json = new JsonValidationResponseParser().parse(response);
                return json.getServiceResponse().getAuthenticationSuccess().getProxies();
            } catch (final Exception e) {
                //失败了也会默认调用XML的解析一次
                logger.warn("Unable to locate proxies from the JSON response", e);
                return super.parseProxiesFromResponse(response);
            }
        }
    
    • Cas20ProxyReceivingTicketValidationFilter

    处理ticket过滤器,默认用 Cas20ServiceTicketValidatorCas20ProxyTicketValidator 处理
    里面有个 proxyGrantingTicketStorage 属性,默认是用 ProxyGrantingTicketStorageImpl实现的,这个是靠一个定时任务去清除过期ticket的
    在初始化的时候定义了一个定时任务去定时清理

        public void init() {
            super.init();
            CommonUtils.assertNotNull(this.proxyGrantingTicketStorage, "proxyGrantingTicketStorage cannot be null.");
    
            if (this.timer == null) {
                this.timer = new Timer(true);
            }
    
            if (this.timerTask == null) {
                this.timerTask = new CleanUpTimerTask(this.proxyGrantingTicketStorage);
            }
            this.timer.schedule(this.timerTask, this.millisBetweenCleanUps, this.millisBetweenCleanUps);
        }
    

    initInternal 方法也是对 该 proxyGrantingTicketStorage的一些参数处理,比如:加密的方法,密钥;还有设定定时器循环时间.
    这个类主要还覆写了getTicketValidator方法,该方法是在父类 AbstractTicketValidationFilter定义,这个决定了认证的方法
    这里对Cas20ServiceTicketValidator做了一些特殊处理,代码如下:

        /**
         * Constructs a Cas20ServiceTicketValidator or a Cas20ProxyTicketValidator based on supplied parameters.
         *
         * @param filterConfig the Filter Configuration object.
         * @return a fully constructed TicketValidator.
         */
        protected final TicketValidator getTicketValidator(final FilterConfig filterConfig) {
            final boolean allowAnyProxy = getBoolean(ConfigurationKeys.ACCEPT_ANY_PROXY);
            final String allowedProxyChains = getString(ConfigurationKeys.ALLOWED_PROXY_CHAINS);
            final String casServerUrlPrefix = getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX);
            final Class<? extends Cas20ServiceTicketValidator> ticketValidatorClass = getClass(ConfigurationKeys.TICKET_VALIDATOR_CLASS);
            final Cas20ServiceTicketValidator validator;
    
            if (allowAnyProxy || CommonUtils.isNotBlank(allowedProxyChains)) {
                final Cas20ProxyTicketValidator v = createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix,
                        this.defaultProxyTicketValidatorClass);
                //处理代理的参数       
                v.setAcceptAnyProxy(allowAnyProxy);
                v.setAllowedProxyChains(CommonUtils.createProxyList(allowedProxyChains));
                validator = v;
            } else {
                validator = createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix,
                        this.defaultServiceTicketValidatorClass);
            }
            validator.setProxyCallbackUrl(getString(ConfigurationKeys.PROXY_CALLBACK_URL));
            //赋值init方法里面初始化的存储方式
            validator.setProxyGrantingTicketStorage(this.proxyGrantingTicketStorage);
    
            final HttpURLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(),
                    getSSLConfig());
            validator.setURLConnectionFactory(factory);
    
            validator.setProxyRetriever(new Cas20ProxyRetriever(casServerUrlPrefix, getString(ConfigurationKeys.ENCODING), factory));
            validator.setRenew(getBoolean(ConfigurationKeys.RENEW));
            validator.setEncoding(getString(ConfigurationKeys.ENCODING));
            //添加用户自定义的参数
            final Map<String, String> additionalParameters = new HashMap<String, String>();
            final List<String> params = Arrays.asList(RESERVED_INIT_PARAMS);
    
            for (final Enumeration<?> e = filterConfig.getInitParameterNames(); e.hasMoreElements(); ) {
                final String s = (String) e.nextElement();
    
                if (!params.contains(s)) {
                    additionalParameters.put(s, filterConfig.getInitParameter(s));
                }
            }
    
            validator.setCustomParameters(additionalParameters);
            return validator;
        }
    
    • Cas20ProxyRetriever:

    这个类主要是为CasProxyProfile提供getProxyTicketFor功能
    声明了getProxyTicketIdFor
    proxyGrantingTicketId 就是 proxyGrantingTicketStorage 存起来的 ,根据 proxyGrantingTicketIou来获取,
    而 proxyGrantingTicketIou来获取 是从 validate的请求返回的XML(JSON)文件信息获取的,代码是:
    XmlUtils.getTextForElement(response, "proxyGrantingTicket");



    • AssertionThreadLocalFilter

    Assertion信息放到 AssertionHolder 里面, AssertionHolder 是一个 ThreadLocal

        public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
                final FilterChain filterChain) throws IOException, ServletException {
            final HttpServletRequest request = (HttpServletRequest) servletRequest;
            final HttpSession session = request.getSession(false);
            final Assertion assertion = (Assertion) (session == null ? request
                    .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
                    .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
    
            try {
                AssertionHolder.setAssertion(assertion);
                filterChain.doFilter(servletRequest, servletResponse);
            } finally {
                AssertionHolder.clear();
            }
        }
    
    • DelegatingFilter

    代理Filter,根据某个参数和设定的 Map<String, Filter> delegators 匹配,匹配上就用某一个过滤器.

    tips:

          //是否就是某一个类
            public boolean exactMatch(final Throwable e) {
                return this.className.equals(e.getClass());
            }
          //判断是否继承自某一个类
            public boolean inheritanceMatch(final Throwable e) {
                return className.isAssignableFrom(e.getClass());
            }
    
    • 用到的认证实体关系
    AttributePrincipal.png

    Assertion:包含认证的时间和 AttributePrincipal
    AttributePrincipal是一个接口,继承自Principal 定义了一个Map属性和getProxyTicketFor方法,实现类是AttributePrincipalImpl
    SimplePrincipal 实现了 Principal
    AssertionPrincipal 继承自 SimplePrincipal 并且包含了 Assertion

    相关文章

      网友评论

        本文标题:CAS client 源码分析

        本文链接:https://www.haomeiwen.com/subject/hmhpdftx.html