CAS client 源码分析

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容