【原创申明:文章为原创,欢迎非盈利性转载,但转载必须注明来源】

之前写过一篇文章,介绍单点登录的基本原理。这篇文章重点介绍开源单点登录系统CAS的登录和注销的实现方法。并结合实际工作中碰到的问题,探讨在集群环境中应用单点登录可能会面临的问题。这篇文章在上一篇的基础上,增加了第四部分,最终的解决方案。

1 单点登录的过程

为了描述方便,假设有如下一个单点登录系统。一套CASServer,两套CAS Client系统。为了描述的方便,省略CAS Server调用用户系统完成登录,以及CASClient从用户系统读取用户详细信息的过程。

1.1 多应用情况下Session信息

假定有两个CAS Client应用,一个CAS Server。应用的部署,可能在不同的服务器,也可能有不同的访问IP或域名,即使是同一个浏览器,在各个应用中的Session信息也是不相同的。

浏览器中,每个应用有一个独立的JSESSIONIDCookie。某一个应用,不可能读取到浏览器在其他应用中的Cookie信息。

假定用户首先访问CAS Client 01,系统提醒用户进行一次登录;然后用户访问CAS Client2,不会再提示登录而是直接登录成功。

1.2 第一次访问CAS Client 01

用户打开浏览器后第一次访问,重定向到单点登录后,会提示用户输入账号密码登录。登录成功之后,再跳转回CAS Client。

1.3 第一次访问CAS Client 02

当用户浏览器已经登录系统,切换到另一个CASClient时,跟第一次访问有所不同,因为已经登录成功,就不会再提醒输入账号密码登录了。

1.4 再次访问CAS Clients

当用户已经访问过CAS Client后,当用户再次访问,系统不会再跳转到CAS Server做认证。

1.5 CASClient配置

为了实现前述的单点登录过程,以Java WEB项目为例,需要在 web.xml 中进行相应的配置。(为了排版,没有填写Filter的完整class名,请自行查阅补充。)

<filter>

<filter-name>CAS AuthenticationFilter</filter-name>

<filter-class>*.AuthenticationFilter</filter-class>

</filter>

<filter>

<filter-name>CAS Validation Filter</filter-name>

<filter-class>*.Cas10TicketValidationFilter</filter-class>

</filter>

<filter>

<filter-name>CAS HttpServletRequest WrapperFilter</filter-name>

<filter-class>*.HttpServletRequestWrapperFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>CAS Validation Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<filter-mapping>

<filter-name>CAS AuthenticationFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<filter-mapping>

<filter-name>CAS HttpServletRequest WrapperFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

仔细看一下配置过滤器可以发现,三个过滤器正好对应流程图中三次访问CAS Client。

  • Authentication Filter:负责将未登录用户跳转到登录界面

  • Authentication Filter:负责验证Service Ticket

  • HttpServletRequest WrapperFilter:负责将用户信息封装到request和session中。

2 统一注销的过程

2.1 不能实现统一注销会有什么问题

当用户访问系统后从系统注销,如何能够从每个应用中都注销?注意前面1.4部分的描述,如果用户注销时,并没有注销CASClient 02中的会话信息,如果用户在浏览器中直接访问这个应用,因为Session存在,并不会提醒用户重新登录。

这会带来两个潜在的隐患:

1、  用户注销user1后换账号user2重新登录,进入CAS Client 02之后,当前身份其实还是user1,并没有如用户预期一样使用user2身份。

2、  用户user1点击注销后离开,没有关闭浏览器。这时候其他用户直接打开CAS Client 02,能够直接盗用user1的身份进行操作。

2.2基本概念:Service、TGT和ST

CAS已经考虑到统一注销的问题。

这里有三个重要的概念TGT、ST和Service,需要着重介绍一下,因为它们同后续统一注销的方案息息相关。

2.2.1  Service

这是用户第一次访问CAS Client的URL。假设一个CAS Client应用部署在域名oa.company.com,使用HTTP协议,应用首页是index.htm。当用户第一次访问这个应用时,对应的URL地址是 http://oa.company.com/index.htm 。这个URL,对CAS Server来说,就是一个service。

当用户第一次跳转到CAS Server的时候,可以看到传了一个参数service,就是这个值。当CASServer生成Ticket重定向到CAS Client的时候,实际就是在这个service 中添加了一个参数 ticket 。

2.2.2   TGT:Ticket Grangting Ticket

TGT是CAS Server为每一个登录用户创建的登录令牌。在CASServer上拥有了TGT,用户就可以证明自己在CASServer成功登录过。TGT封装了SessionCookie值以及此Cookie值对应的用户信息。当HTTP请求到来时,CAS以此Cookie值为key查询缓存中有无TGT ,如果有的话,则相信用户已登录过。

2.2.3  ST:Service Ticket

ST是CAS Server为用户签发的访问某一service的认证令牌。用户访问service时,service发现用户没有ST,浏览器会跳转到CASServer去获取ST。CAS Server发现用户有TGT,则签发一个ST,返回给用户。用户使用ST作为ticket参数去访问service,service拿ST去CAS Server验证,验证通过后,得到当前登录用户的登录名。

注意TGT和ST,是一对多的关系。一个TGT会维护一个 services 列表,每当为用户创建一个ST并认证通过后,会将这个ST添加到TGT的services列表中。这样,在CASServer端,这个services列表实际维护了一个用户登录过的所有CASClient。这就为实现统一注销打下了基础。

2.3 CAS Client的统一注销配置

CAS Client,为了实现统一注销,除了第一张介绍的三个登录过程的过滤器之外,还需要添加一个统一注销过滤器。

<filter>

<filter-name>CAS Single Sign OutFilter</filter-name>

<filter-class>*.SingleSignOutFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>CAS Single Sign OutFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<listener>

<listener-class>*.SingleSignOutHttpSessionListener</listener-class>

</listener>

2.4 CAS Server注销过程

用户在浏览器中点击“注销”链接,实际浏览器会访问CASServer的注销页面。收到注销请求后,CAS Server会读取到TGT,并检查当前用户登录过的所有service,并依次发送注销请求。

2.5 CAS Client注销过程

CAS Client的注销,核心代码是SingleSignOutFilter,它的关键代码

public voiddoFilter(servletRequest, servletResponse, filterChain){

HttpServletRequest request =(HttpServletRequest)servletRequest;

if (handler.isTokenRequest(request)) {

handler.recordSession(request);

} else if (handler.isLogoutRequest(request)) {

handler.destroySession(request);

return;

}

filterChain.doFilter(servletRequest, servletResponse);

}

其中handler是SingleSignOutHandler的实例,这个对象完成用户在CASClient端登录信息的维护和注销工作。

至此,CAS完整的登录和注销过程就完成。

2.6 思考:什么情况统一注销会失败

统一注销的实现,需要CAS Server通过HttpClient访问CAS Client的service。如果这个访问过程失败,就会导致统一注销失败。列了几种情况,不详述。

1、开发调试阶段,使用localhost访问CAS Client。

2、CAS Server部署在外网,CAS Client部署在内网。

3、网络安全设置,不允许CASServer访问CAS Client。

3 CAS Client集群的影响

前面的论述,一直假定所有的CAS Client都是单点部署,没有集群。如果集群,会有什么影响,应该如何来解决?

3.1 Client集群对登录的影响

假设使用nginx做集群前端,后面部署两台CAS Client 01的实例。我们看看对登录过程会有什么影响。

为了描述方便,CAS Client登录过程会有三次请求(对应三个过滤器),我们依次命名为Authentication Request / Validation Request / Wrapper Request。

Nginx缺省的分发规则,并不是sticky模式,同一个浏览器的请求,会按照nginx自身某种规则进行分发。我们曾经测试过,在双点集群环境下,Authentication Request和ValidationRequest会恰好被分发到两台服务器,这就会导致登录过程死循环。

出现登录死循环的原因,主要在于nginx分发时,没有使用sticky策略,也就是同一个浏览器的请求,永远分发给同一台CAS Client实例。缺省nginx的分发策略,可以根据用户IP分发,实现的是同一个IP永远分发到同一台Client,这样就能解决死循环的问题。

3.2 Client集群对注销的影响

当nginx实现了sitcky转发,同一个浏览器的访问会分发到同一个Client1实例,该用户的会话信息也一直保存在Client1实例中。

当用户统一注销时,由CAS Server向Client发送注销请求,这时候nginx无法确保按当前用户进行分发,因此可能会被分发到Client2。这时候,实际效果是注销失败。

这个问题,在我们当前的环境中真实存在,还没有合理的解决方法。初步分析,大概有几个修改方向。

3.2.1  修改nginx分发策略

问题存在的原因,是因为nginx在分发注销策略时,不能准确分发。如果能在这个环节进行修改,系统代码和环境,基本不用做任何修改。

这里有两种分发方法:

l CAS Server发送的注销请求,分发给对应的后台服务器。

l CAS Server发送的注销请求,广播到所有的后台服务器。

初步结论:同架构组进行了沟通,这两种方案都很难实现,特别是广播的方案,没在网络上找到类似成功的案例。

3.2.2  集群的节点实现Session同步

如果能实现集群Session的同步:同步创建、同步注销,主要在一个Client上实现了注销,其他Client也就同步注销。

这个会对Tomcat性能有影响。

3.2.3  集群节点使用redis保存会话信息

即使是多个节点,它们的会话信息只有一份。一旦失效,则所有节点都失效。这只是一个设想,没有做技术调研,不知能够实现。

这有两种修改方法:

l 修改Tomcat的配置文件,使用redis保存Tomcat的会话信息。

l 修改代码而不是Tomcat,使用redis保存会话信息。

初步结论:架构组不允许修改生产环境的Tomcat,否定了第一种方法。我们只能尝试修改代码并利用redis保存会话。

3.2.4  每次请求验证用户是否注销

首先,在CAS Server中实现一个接口,用于判断某一个ST对应的TGT是否还有效。

在SingleSignOutFilter中,每次访问都调用CAS Server的这个新接口,判断用户是否已经注销。如果已经注销,则立刻注销本实例中的会话信息。

这个方法是比较安全的解决办法,但每次请求都会调用CASServer接口,会对性能造成巨大影响。完全不建议用这种方案。

3.2.5  几种策略的初步调研

对前面提到的几种方案做了初步调研之后:

l 技术实现困难,否定了方案1

l 性能考虑以及架构组的策略,否定方案2

l 架构组的策略,否定方案3中的第一种做法。

l 性能考虑,否定方案4。

因此,可能的做法是修改代码,使用redis保存会话信息。

四  使用redis保存会话

在目前的生产环境的限制下,我们只能采用修改代码来实现redis保存会话的实现方案。

4.1 Request和Session缺省怎么实现

在Tomcat缺省的实现中,Session信息都是保存在JVM中,所以不能跨JVM共享。

要想将所有的session都保存到redis中,一种能想到的简单办法是自己写一个CustomSession,将会话信息保存到这个自定义的Session中,并且利用redis等进行保存。但这样做,会带来很大的代码改动,所有涉及到session读写操作的地方可能都需要修改。

我们希望找到更优雅的解决方案,能够修改更少的代码。

4.2 WEB请求的执行过程

Request 和Session什么时候创建?如何传递?

Filter的调用入口函数是doFilter,传入的主要参数是request和response。在此之前,Tomcat已经创建好request。通常情况下,业务代码不需要关心request和session等对象如何创建的问题,只需要使用即可。每个过滤器的实现,当需要继续流程的时候,只需要将得到的request和response传递给下一个filter就行。

但这仅仅是缺省做法,并不表示我们不能修改或重写一个request对象。我们想修改Session的保存位置,如果能在所有的Filter之前插入一个自定义过滤器,定义一个新的Request传递给后面的Filter,并且让后面的Filter和Servlet感受不到变化,就可以实现这个目标。

4.3 如何定制Request

4.3.1  增加过滤器

在所有的Filter之前,插入一个新的Filter。

HttpServletRequest可以重写吗?

4.3.2  Tomcat的Request实现

4.3.3  改写之后的Request实现

在Session重写一个RedisSessionRequest,继承自HttpServletRequestWrapper,并包含原request(RequestFacade)的引用。但需要读取Form参数时,直接调用oriRequest取值。当需要拿到Session对象进行会话信息访问时,调用重载后的函数。

这样就实现了request的封装,在后续的filter和servlet中通过request获取到的session,都是放在redis中的会话数据,不再是缺省保存在JVM中的数据。

4.3.4  集群环境的session读写

当nginx将同一个浏览器的请求分发给不同的Tomcat时,都会根据SessionId从redis中读取Session。因为同一个浏览器发送请求的SessionID相同,所以在不同的Tomcat实例中,会读取到同一个Session对象。

4.4 使用Spring Session实现

根据前面的分析,在项目中自定义Request,就可以实现需求。Spring Session已经是一个成熟的开源实现,并且后端实现了将会话保存在redis、mongodb、jdbc等多种实现,我们没必要自己发明轮子。

Spring提供的例子代码很简洁,跟我们已经实现的业务系统稍微有点不同。在现有系统中,已经定义了bean jedisConnectionFactory,可以直接使用。

4.4.1  修改pom.xml

在pom.xml文件中,添加代码

<dependency>

<groupId>org.springframework.session</groupId>

<artifactId>spring-session-data-redis</artifactId>

<version>1.2.0.RELEASE</version>

</dependency>

4.4.2  修改redis配置文件

在项目中已经有redis配置文件spring-redis.xml,在其中添加内容

<context:annotation-config/>

<beans:beanclass="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>

4.4.3  修改web.xml

在所有的过滤器前面添加一个新的过滤器

<filter>

<filter-name>springSessionRepositoryFilter</filter-name>

<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

</filter>

<filter-mapping>

<filter-name>springSessionRepositoryFilter</filter-name>

<url-pattern>/*</url-pattern>

<dispatcher>REQUEST</dispatcher>

<dispatcher>ERROR</dispatcher>

</filter-mapping>

4.5 测试实现效果

集成Spring Session后,经过初步测试,能够达到预想效果。(感谢同事瑞钊的实际测试并提供截图)

4.5.1  Session信息已经保存到redis中

用户登录后查看redis中的数据,可以看到这些Session信息。

4.5.2  删除redis中会话的影响

用户登录后继续访问系统,不会切换到CAS登录页面。

如果手工删掉redis中的session,重新访问,可以看到需要重新做一个CAS认证的过程。

4.5.3  统一用户注销的测试

后续需要部署一套生产环境的集群环境,验证统一注销的效果。经过前面两步测试验证,理论上说注销已经不是问题。

CAS Client集群环境的Session问题及解决方案的更多相关文章

  1. 基于CentOS与VmwareStation10搭建Oracle11G RAC 64集群环境:4.安装Oracle RAC FAQ-4.4.无法图形化安装Grid Infrastructure

    无法图形化安装: [grid@linuxrac1 grid]$ ./runInstaller Starting Oracle Universal Installer... Checking Temp ...

  2. (转)深入探讨在集群环境中使用 EhCache 缓存系统

    简介: EhCache 是一个纯 Java 的进程内缓存框架,具有快速.精干等特点,是 Hibernate 中默认的 CacheProvider.本文充分的介绍了 EhCache 缓存系统对集群环境的 ...

  3. 深入探讨在集群环境中使用 EhCache 缓存系统

    EhCache 缓存系统简介 EhCache 是一个纯 Java 的进程内缓存框架,具有快速.精干等特点,是 Hibernate 中默认的 CacheProvider. 下图是 EhCache 在应用 ...

  4. Apache httpd和JBoss构建高可用集群环境

    1. 前言 集群是指把不同的服务器集中在一起,组成一个服务器集合,这个集合给客户端提供一个虚拟的平台,使客户端在不知道服务器集合结构的情况下对这一服务器集合进行部署应用.获取服务等操作.集群是企业应用 ...

  5. 基于CentOS与VmwareStation10搭建Oracle11G RAC 64集群环境:2.搭建环境-2.6. 安装Oracle所依赖的必要包

    2.6. 安装Oracle所依赖的必要包 2.6.1. 检查Oracle所依赖的必要rpm包 [root@localhost /]#rpm -q binutils compat-libstdc elf ...

  6. 基于CentOS与VmwareStation10搭建Oracle11G RAC 64集群环境:4.安装Oracle RAC FAQ-4.6.重新配置与缷载11R2 Grid Infrastructure

    1.[root@linuxrac1 ~]# /u01/app/oraInventory/orainstRoot.sh 2.[root@linuxrac2 ~]# /u01/app/oraInvento ...

  7. Apache + Tomcat + mod_jk实现集群服务及session共享

    实现效果:用apache 分发请求到tomcat中的对应的项目 原理:

  8. hadoop集群环境搭建之安装配置hadoop集群

    在安装hadoop集群之前,需要先进行zookeeper的安装,请参照hadoop集群环境搭建之zookeeper集群的安装部署 1 将hadoop安装包解压到 /itcast/  (如果没有这个目录 ...

  9. CentOS7 安装Hadoop集群环境

    先按照上一篇安装与配置好CentOS以及zookeeper http://www.cnblogs.com/dopeter/p/4609276.html 本章介绍在CentOS搭建Hadoop集群环境 ...