《SSO系列三》CAS集群部署时session异常

遇到的问题

今年第三季度跳了一次槽,现在做的项目前端以web为主,这个项目用到了SSO,这也是我第一次接触SSO。最近,公司把服务器迁移到微软云的时候顺便把容器部署了多个实例,前端做负载均衡。
在迁移之前,我们还查看了一下代码里没有用session。当部署多个tomcat,负载均衡用轮训策略的时候。在项目里点击登录,跳转到SSO的登录界面,在SSO登录成功以后,调回到我们的服务器时失败,浏览器返回重定向过多,请求失败。从访问日志上看,多个tomcat轮流被访问了多次。由于大家对SSO都不熟,在纠结了两天之后,选择了退而求其次的方法,把负载均衡的分配算法改为IP轮训,保证同一个用户访问同一个tomcat。这也是我开始了解、学习SSO的一个原因。

CAS是什么

公司项目里使用的是CAS来实现的SSO,CAS是实现SSO最常用的开源项目。它的官方介绍是这么一句

Enterprise Single Sign-On - CAS provides a friendly open source community that actively supports and contributes to the project. While the project is rooted in higher-ed open source, it has grown to an international audience spanning Fortune 500 companies and small special-purpose installations.

这篇blog的特点

学习SSO的过程中也查看过一些其他的博客,以介绍SSO的概念、流程的居多,以文字叙述为主。这篇文章从cas-client源码的角度去解释CAS的如何工作的。注意这篇文章不是介绍cas应该如何集成、如何使用的,如果你还不会使用cas,可以先找一个教程去学习一下cas如何集成。

cas-client

我写这篇blog时,基于cas-server 4.1.0、server-client 3.3.3
使用cas-client,需要在web.xml配置拦截器。配置后的web.xml如下所示:


<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListenerlistener-class>
    listener>
    <filter>
        <filter-name>CAS Single Sign Out Filterfilter-name>
        <filter-class>org.jasig.cas.client.session.SingleSignOutFilterfilter-class>
    filter>
    <filter-mapping>
        <filter-name>CAS Single Sign Out Filterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    <filter>
        <filter-name>CAS Authentication Filterfilter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilterfilter-class>
        <init-param>
            <param-name>casServerLoginUrlparam-name>
            <param-value>http://www.cas.com:8080/loginparam-value>
        init-param>
        <init-param>
            <param-name>serverNameparam-name>
            <param-value>http://www.casclient.com:8081param-value>
        init-param>
    filter>
    <filter-mapping>
        <filter-name>CAS Authentication Filterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    <filter>
        <filter-name>CAS Validation Filterfilter-name>
        <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilterfilter-class>
        <init-param>
            <param-name>casServerUrlPrefixparam-name>
            <param-value>http://www.cas.com:8080param-value>
        init-param>
        <init-param>
            <param-name>serverNameparam-name>
            <param-value>http://www.casclient.com:8081param-value>
        init-param>
    filter>
    <filter-mapping>
        <filter-name>CAS Validation Filterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    <filter>
        <filter-name>CAS HttpServletRequest Wrapper Filterfilter-name>
        <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilterfilter-class>
    filter>
    <filter-mapping>
        <filter-name>CAS HttpServletRequest Wrapper Filterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    <context-param>
        <param-name>contextConfigLocationparam-name>
        <param-value>
            classpath:applicationContext.xml
        param-value>
    context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
    listener>
web-app>

web.xml文件里配置了4个Filter。分别是:SingleSignOutFilter、AuthenticationFilter、Cas20ProxyReceivingTicketValidationFilter、HttpServletRequestWrapperFilter。

SingleSignOutFilter

SingleSignOutFilter这个Filter主要处理当请求时退出请求时,重定向到对应的Url。

AuthenticationFilter

AuthenticationFilter这个Filter的核心代码如下:

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;
    if (this.isRequestUrlExcluded(request)) {
        this.logger.debug("Request is ignored.");
        filterChain.doFilter(request, response);
    } else {
        HttpSession session = request.getSession(false);
        Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
        if (assertion != null) {
            filterChain.doFilter(request, response);
        } else {
            String serviceUrl = this.constructServiceUrl(request, response);
            String ticket = this.retrieveTicketFromRequest(request);
            boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
            if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
                this.logger.debug("no ticket and no assertion found");
                String modifiedServiceUrl;
                if (this.gateway) {
                    this.logger.debug("setting gateway attribute in session");
                    modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
                } else {
                    modifiedServiceUrl = serviceUrl;
                }
                this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
                String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
                this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
                this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }
}
  • 1、第4行,如果是不需要登录的请求,进入处理链的下一级。
  • 2、第9行获取session里Key为const_cas_assertion的对象,使用不自动创建session的方法,现在给这个对象命名叫assertion。
  • 3、如果第2步中的assertion存在,那么进入处理链的下一级。
  • 4、如果第2步中的assertion不存在,那么在第14行获取请求参数是Key为ticket的值,现在给这个对象命名叫ticket。
  • 5、如果第4步中的ticket存在,那么进入处理链的下一级。
  • 6、如果第4步中的ticket不存在,那么重定向到cas-server的登录界面。

总结一下:这个Filter的作用就是对于需要登录的请求,如果这个请求session里没有const_cas_assertion对象或者参数里不包含ticket属性,那么重定向到cas-server的登录界面。其他情况都进入处理链的下一个处理。

Cas20ProxyReceivingTicketValidationFilter

Cas20ProxyReceivingTicketValidationFilter这个Filter的核心代码如下:

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    if (this.preFilter(servletRequest, servletResponse, filterChain)) {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        String ticket = this.retrieveTicketFromRequest(request);
        if (CommonUtils.isNotBlank(ticket)) {
            this.logger.debug("Attempting to validate ticket: {}", ticket);
            try {
                Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
                this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
                request.setAttribute("_const_cas_assertion_", assertion);
                if (this.useSession) {
                    request.getSession().setAttribute("_const_cas_assertion_", assertion);
                }
                this.onSuccessfulValidation(request, response, assertion);
                if (this.redirectAfterValidation) {
                    this.logger.debug("Redirecting after successful ticket validation.");
                    response.sendRedirect(this.constructServiceUrl(request, response));
                    return;
                }
            } catch (TicketValidationException var8) {
                this.logger.debug(var8.getMessage(), var8);
                this.onFailedValidation(request, response);
                if (this.exceptionOnValidationFailure) {
                    throw new ServletException(var8);
                }
                response.sendError(403, var8.getMessage());
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}
  • 1、第6行可以看到对于ticket不为空的情况进行了第7行到第30行的处理,对于ticket为空的情况直接进入处理链的下一级。以为能进入这个Filter的请求,并且ticket为空的,只有不需要登录和已经登录(色素四栋包含const_cas_assertion这个对象)这两种情况,这两种情况都可以进入处理链的下一级。
  • 2、第9行,使用ticket去cas-server去验证ticket的真假,并可以获取用户的基本信息,封装成Assertion对象返回来,可以理解Assertion就是获取的用户信息。如果获取失败,那么执行catch语句,返回403没有权限。
  • 3、第12行,如果cas-client使用session的的话,把assertion存放在session里Key为const_cas_assertion里,以标记用户已经登录成功了。上面AuthenticationFilter代码里在session里取的也是这个对象。
  • 4.第16行,如果redirectAfterValidation这个参数配置为true的话,返回给客户端302,让客户端重定向一次,重定向的地址还是这个地址。这个参数就是控制,当cas-client想cas-server验证ticket的真伪后是否要做一次重定向。

总结一下:这个Filter就是在需要验证ticket不为空时,去cas-serer验证验证ticket是否有效。并且适当的缓存session和重定向到当前请求。

HttpServletRequestWrapperFilter

Cas20ProxyReceivingTicketValidationFilter这个Filter就是做了一下封装,让用户方便获取cas-client管理的用户基本信息,没有其他的逻辑。

总结

可以看到cas-client是使用了session来标记用户身份的,只是全都封装在Filter这层,不需要用户操作。对于已经登录的请求,可以到Servlet处理;没有登录的已经重定向出去了,不出调用servlet的方法。用户在servlet方法里调用request.getUserPrincipal()就可以获取用户信息了。

我司CAS集群时遇到的问题

environment

两个tomcat,负载均衡器轮训去调度,sso-client里配置use-session=true,redirectAfterValidation=true。

原因

  • 1、当用户调用登录接口时,这时没有session、没有ticket,被处理的流程是:SingleSignOutFilter->AuthenticationFilter,在AuthenticationFilter过滤器重定向到sso-server登录。
  • 2、在sso-server登录成功后,重定向到业务服务器,然后携带ticket参数。这时处理的流程是:SingleSignOutFilter->AuthenticationFilter->Cas20ProxyReceivingTicketValidationFilter,在Cas20ProxyReceivingTicketValidationFilter里面第9行通过ticket获取assertion。由于redirectAfterValidation=true,会执行18行里的重定向,再重定向到当前的请求。
  • 3、由于负载均衡的轮训策略,请求会被分到另一个tomcat中去,由于在另一个tomcat里没有session(如果没有在这个tomcat上登录过肯定是没有;如果在这个tomcat上登录过也不会有,因为另一个tomcat已经把它的JSESSIONID给替换掉了。),且没有携带ticket,所以会重复执行前面的步骤,不能自拔。

解法

现在问题就比较简单了,就是session集群的事情嘛。
集中处理session或者IP负载均衡策略。

你可能感兴趣的:(♚java♚,♚服务器♚)