前面的文章中介绍了 Spring Security 如何像微信 一样,自动踢掉已登录用户,但是那是基于单体应用的,如果我们的项目是集群化部署,这个问题该如何解决呢?
今天我们就来看看集群化部署,Spring Security 要如何处理 session 并发。
1.集群会话方案
在传统的单服务架构中,一般来说,只有一个服务器,那么不存在 Session 共享问题,但是在分布式/集群项目中,Session 共享则是一个必须面对的问题,先看一个简单的架构图:

在这样的架构中,会出现一些单服务中不存在的问题,例如客户端发起一个请求,这个请求到达 Nginx 上之后,被 Nginx 转发到 Tomcat A 上,然后在 Tomcat A 上往 session 中保存了一份数据,下次又来一个请求,这个请求被转发到 Tomcat B 上,此时再去 Session 中获取数据,发现没有之前的数据。
session 共享
对于这一类问题的解决,目前比较主流的方案就是将各个服务之间需要共享的数据,保存到一个公共的地方(主流方案就是 Redis):
当所有 Tomcat 需要往 Session 中写数据时,都往 Redis 中写,当所有 Tomcat 需要读数据时,都从 Redis 中读。这样,不同的服务就可以使用相同的 Session 数据了。
这样的方案,可以由开发者手动实现,即手动往 Redis 中存储数据,手动从 Redis 中读取数据,相当于使用一些 Redis 客户端工具来实现这样的功能,毫无疑问,手动实现工作量还是蛮大的。
一个简化的方案就是使用 Spring Session 来实现这一功能,_Spring Session 就是使用 Spring 中的代理过滤器,将所有的 Session 操作拦截下来,自动的将数据 同步到 Redis 中_,或者自动的从 Redis 中读取数据。
对于开发者来说,所有关于 Session 同步的操作都是透明的,开发者使用 Spring Session,一旦配置完成后,具体的用法就像使用一个普通的 Session 一样。
session 拷贝
session 拷贝就是不利用 redis,直接在各个 Tomcat 之间进行 session 数据拷贝,但是这种方式效率有点低,Tomcat A、B、C 中任意一个的 session 发生了变化,都需要拷贝到其他 Tomcat 上,如果集群中的服务器数量特别多的话,这种方式不仅效率低,还会有很严重的延迟。所以这种方案一般作为了解即可。
粘滞会话
所谓的粘滞会话就是将相同 IP 发送来的请求,通过 Nginx 路由到同一个 Tomcat 上去,这样就不用进行 session 共享与同步了。这是一个办法,但是在一些极端情况下,可能会导致负载失衡(因为大部分情况下,都是很多人用同一个公网 IP)。
综上分析 Session 共享就成为了这个问题目前主流的解决方案了。
Session共享方案的实现
引入项目需要的依赖
1 | <!--session用redis存储实现共享--> |
配置redis
1 | spring: |
新增Controller接口
在HelloController中添加如下配置
1 | ("${server.port}") |
注意记得为Cos和Client等实体类实现serializable接口, 否则会报无法序列化的错.
考虑到要模拟以集群的方式启动 Spring Boot项目, 为了获取每一个请求到底是哪一个 Spring Boot 提供的服务,需要在每次请求时返回当前服务的端口号,因此这里注入了 server.port 。
用默认yaml配置启动项目:
复制配置以9998端口再次启动:


Nginx配置反向代理
修改host文件定制域名
如果有特殊的域名需要配置要在host指明, 此处为了简单就用localhost了
nginx.conf文件添加反向代理
执行sudo nginx -t查看nginx配置文件路径.
1 | upstream selfDefinedLB { |
再次执行sudo nginx -t确认修改无误.
重启/启动nginx
访问localhost/hello能看见下面40的返回说明nginx配置已经生效.
Security 配置
Session 共享已经实现了,但是我们发现新的问题: 之前配置的session 并发管理失效了。 也就是说,配置:
1 | protected void configure(HttpSecurity http) throws Exception { |
失效, 测试两个浏览器访问http://localhost/login.html 并登陆发现都能登陆上(如下图), 并不会报只能有一个session的错.
TO-DO
不再能实现同一个用户只允许端登陆了. 具体原因可以参见文章
在该文中,我们提到,会话注册表的维护默认是由 SessionRegistryImpl 来维护的,而 SessionRegistryImpl 的维护就是基于内存的维护。现在我们虽然启用了 Spring Session+Redis 做 Session 共享,但是 SessionRegistryImpl 依然是基于内存来维护的,所以我们要修改 SessionRegistryImpl 的实现逻辑。
修改方式也很简单,实际上 Spring Session 为我们提供了对应的实现类 SpringSessionBackedSessionRegistry,具体配置如下:
1 | // |
重启项目发现报错如下
具体报错内容:
1 | //Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate |
原因是之前处理踢掉登陆用户的逻辑时加过一个配置
1 |
|
目前spring-session-data-redis接管了session并发管理, 和之前配置的session事件发布器有冲突, 将之前的HttpSessionEventPublisher注释即可。
注释后再次重启项目成功.
在两个浏览器使用同一个Benedict访问登陆接口, 发现注释掉监听器HttpSessionEventPublisher后session并发管理重新生效
访问set/get方法也能看见nginx反向代理生效中.
