今年第三季度跳了一次槽,现在做的项目前端以web为主,这个项目用到了SSO,这也是我第一次接触SSO。最近,公司把服务器迁移到微软云的时候顺便把容器部署了多个实例,前端做负载均衡。
在迁移之前,我们还查看了一下代码里没有用session。当部署多个tomcat,负载均衡用轮训策略的时候。在项目里点击登录,跳转到SSO的登录界面,在SSO登录成功以后,调回到我们的服务器时失败,浏览器返回重定向过多,请求失败。从访问日志上看,多个tomcat轮流被访问了多次。由于大家对SSO都不熟,在纠结了两天之后,选择了退而求其次的方法,把负载均衡的分配算法改为IP轮训,保证同一个用户访问同一个tomcat。这也是我开始了解、学习SSO的一个原因。
公司项目里使用的是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.
学习SSO的过程中也查看过一些其他的博客,以介绍SSO的概念、流程的居多,以文字叙述为主。这篇文章从cas-client源码的角度去解释CAS的如何工作的。注意这篇文章不是介绍cas应该如何集成、如何使用的,如果你还不会使用cas,可以先找一个教程去学习一下cas如何集成。
我写这篇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这个Filter主要处理当请求时退出请求时,重定向到对应的Url。
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);
}
}
}
}
总结一下:这个Filter的作用就是对于需要登录的请求,如果这个请求session里没有const_cas_assertion对象或者参数里不包含ticket属性,那么重定向到cas-server的登录界面。其他情况都进入处理链的下一个处理。
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);
}
}
总结一下:这个Filter就是在需要验证ticket不为空时,去cas-serer验证验证ticket是否有效。并且适当的缓存session和重定向到当前请求。
Cas20ProxyReceivingTicketValidationFilter这个Filter就是做了一下封装,让用户方便获取cas-client管理的用户基本信息,没有其他的逻辑。
可以看到cas-client是使用了session来标记用户身份的,只是全都封装在Filter这层,不需要用户操作。对于已经登录的请求,可以到Servlet处理;没有登录的已经重定向出去了,不出调用servlet的方法。用户在servlet方法里调用request.getUserPrincipal()就可以获取用户信息了。
两个tomcat,负载均衡器轮训去调度,sso-client里配置use-session=true,redirectAfterValidation=true。
现在问题就比较简单了,就是session集群的事情嘛。
集中处理session或者IP负载均衡策略。