Spring-Security-10-集群化部署Session管理 | Eloise's Paradise
0%

Spring-Security-10-集群化部署Session管理

前面的文章中介绍了 Spring Security 如何像微信 一样,自动踢掉已登录用户,但是那是基于单体应用的,如果我们的项目是集群化部署,这个问题该如何解决呢?

今天我们就来看看集群化部署,Spring Security 要如何处理 session 并发。

1.集群会话方案

在传统的单服务架构中,一般来说,只有一个服务器,那么不存在 Session 共享问题,但是在分布式/集群项目中,Session 共享则是一个必须面对的问题,先看一个简单的架构图:

传统集群化部署架构
在这样的架构中,会出现一些单服务中不存在的问题,例如客户端发起一个请求,这个请求到达 Nginx 上之后,被 Nginx 转发到 Tomcat A 上,然后在 Tomcat A 上往 session 中保存了一份数据,下次又来一个请求,这个请求被转发到 Tomcat B 上,此时再去 Session 中获取数据,发现没有之前的数据。

session 共享

对于这一类问题的解决,目前比较主流的方案就是将各个服务之间需要共享的数据,保存到一个公共的地方(主流方案就是 Redis):
session共享后的集群化部署架构
当所有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--session用redis存储实现共享-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring session 实现对共享session的自动化管理-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

配置redis

1
2
3
4
spring:
redis:
host: 127.0.0.1
port: 6379

新增Controller接口

在HelloController中添加如下配置

1
2
3
4
5
6
7
8
9
10
11
@Value("${server.port}")
Integer port;
@GetMapping("/set")
public String set(HttpSession session) {
session.setAttribute("user", "Benidict 卷福");
return String.valueOf(port);
}
@GetMapping("/get")
public String get(HttpSession session) {
return session.getAttribute("user") + ":" + port;
}

注意记得为Cos和Client等实体类实现serializable接口, 否则会报无法序列化的错.

考虑到要模拟以集群的方式启动 Spring Boot项目, 为了获取每一个请求到底是哪一个 Spring Boot 提供的服务,需要在每次请求时返回当前服务的端口号,因此这里注入了 server.port 。
用默认yaml配置启动项目:
App启动成功
复制配置以9998端口再次启动:
找到复制配置
复制配置修改端口设置
App以9998端口再次启动

Nginx配置反向代理

修改host文件定制域名

如果有特殊的域名需要配置要在host指明, 此处为了简单就用localhost了

nginx.conf文件添加反向代理

执行sudo nginx -t查看nginx配置文件路径.
nginx测试

configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
upstream selfDefinedLB {
server 127.0.0.1:9999 weight=1;
server 127.0.0.1:9998 weight=1;
}
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://selfDefinedLB; #这里指定负载均衡策略, http://后面是自定义的负载均衡策略, 在上面upstream关键字后指定
}
}

再次执行sudo nginx -t确认修改无误.
重启/启动nginx
访问localhost/hello能看见下面40的返回说明nginx配置已经生效.
hello接口401

Security 配置

Session 共享已经实现了,但是我们发现新的问题: 之前配置的session 并发管理失效了。 也就是说,配置:

1
2
3
4
5
6
7
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest()
...
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);
}

失效, 测试两个浏览器访问
http://localhost/login.html 并登陆发现都能登陆上(如下图), 并不会报只能有一个session的错.
SESSION共享设置失效

TO-DO

不再能实现同一个用户只允许端登陆了. 具体原因可以参见文章

在该文中,我们提到,会话注册表的维护默认是由 SessionRegistryImpl 来维护的,而 SessionRegistryImpl 的维护就是基于内存的维护。现在我们虽然启用了 Spring Session+Redis 做 Session 共享,但是 SessionRegistryImpl 依然是基于内存来维护的,所以我们要修改 SessionRegistryImpl 的实现逻辑。
修改方式也很简单,实际上 Spring Session 为我们提供了对应的实现类 SpringSessionBackedSessionRegistry,具体配置如下:

1
2
3
4
5
6
7
8
9
// 
.sessionRegistry(sessionRegistry())

@Autowired
FindByIndexNameSessionRepository sessionRepository;
@Bean
SpringSessionBackedSessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry(sessionRepository);
}

重启项目发现报错如下

具体报错内容:

1
2
3
4
5
6
//Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate 
// [org.springframework.session.data.redis.RedisIndexedSessionRepository]: Circular reference involving containing
// bean 'org.springframework.boot.autoconfigure.session.RedisSessionConfiguration$SpringBootRedisHttpSessionConfiguration'
// - consider declaring the factory method as static for independence from its containing instance.
// Factory method 'sessionRepository' threw exception; nested exception is java.lang.IllegalStateException:
// RedisConnectionFactory is required

原因是之前处理踢掉登陆用户的逻辑时加过一个配置

1
2
3
4
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

目前spring-session-data-redis接管了session并发管理, 和之前配置的session事件发布器有冲突, 将之前的HttpSessionEventPublisher注释即可。
注释后再次重启项目成功.
在两个浏览器使用同一个Benedict访问登陆接口, 发现注释掉监听器HttpSessionEventPublisher后session并发管理重新生效
同一个用户只允许一个session重新生效
访问set/get方法也能看见nginx反向代理生效中.
set接口
get接口

-------------本文结束感谢您的阅读-------------