概述
遇到的问题
今年第三季度跳了一次槽,现在做的项目前端以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如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<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.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://www.cas.com:8080/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://www.casclient.com:8081</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://www.cas.com:8080</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://www.casclient.com:8081</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-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负载均衡策略。
最后
以上就是复杂哈密瓜为你收集整理的《SSO系列三》CAS集群部署时session异常的全部内容,希望文章能够帮你解决《SSO系列三》CAS集群部署时session异常所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复