7.4 Session 共享
7.4.1 集群会话方案
前面所讲的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案就会失效。需要注意的是,我们这里讨论的范畴是有状态登录,如果用户采用无状态的认证方案,那么就不涉及会话,也就不存在接下来要讨论的问题。
我们先来看一幅简单的集群架构图,如图 7-3 所示。
如图 7-3 所示,如果项目是集群化部署,我们可以采用 Nginx 做反向代理服务器,所有到达 Nginx 上的请求被转发到不同的 Tomcat 实例上,每个 Tomcat 各自保存自己的会话信息。根据前面的讲解,Spring Security 中通过维护一张会话注册表来实现会话的并发管理,现在每个 Tomcat 上都有一张会话注册表,所以如果还按照之前的方式去配置会话并发管理,那必然是不生效的。
图 7-3 简化版的集群架构图
为了解决集群环境下的会话问题,我们有三种方案:
(1)Session 复制:多个服务之间互相复制 Session 信息,这样每个服务中都包含有所有的 Session 信息了,Tomcat 通过 IP 组播对这种方案提供支持。但是这种方案占用带宽、有时延,服务数量越多效率越低,所以这种方案使用较少。
(2)Session 粘滞:也叫会话保持,就是在 Nginx 上通过一致性 Hash,将 Hash 结果相同的请求总是分发到一个服务上去。这种方案可以解决一部分集群会话带来的问题,但是无法解决集群中的会话并发管理问题。
(3)Session 共享:Session 共享就是将不同服务的会话统一放在一个地方,所有的服务共享一个会话。一般使用一些 Key-Value 数据库来存储 Session,例如 Memcached 或者 Redis 等,比较常见的方案是使用 Redis 存储,Session 共享方案由于其简便性与稳定性,是目前使用较多的方案。Session 共享架构图如图 7-4 所示。
图 7-4 简化版的 Session 共享架构图
Session 共享目前使用比较多的是 spring-session,利用 spring-session 可以方便地实现 Session 的管理。
7.4.2 实战
首先启动一个 Redis 实例。
新建 Spring Boot 工程,分别引入 Web、Redis、Spring Security 以及 Spring Session 依赖,代码如下:
<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> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
接下来在 application.properties 中配置 Redis 连接信息:
spring.redis.password=123 spring.redis.host=127.0.0.1 spring.redis.port=6379
再来提供一个 SecurityConfig,代码如下:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired FindByIndexNameSessionRepository sessionRepository; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("javaboy") .password("{noop}123") .roles("admin"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .csrf() .disable() .sessionManagement() .maximumSessions(1) .sessionRegistry(sessionRegistry()); } @Bean SpringSessionBackedSessionRegistry sessionRegistry() { return new SpringSessionBackedSessionRegistry(sessionRepository); } }
在这段配置中,我们首先注入了一个 FindByIndexNameSessionRepository 对象,这是一个会话的存储和加载工具。在前面的案例中,会话信息是保存在内存中的,现在会话信息保存在 Redis 中,具体的保存和加载过程则是由 FindByIndexNameSessionRepository 接口的实现类来完成,默认是 RedisIndexedSessionRepository,即我们一开始注入的实际上是一个 RedisIndexed SessionRepository 类型的对象。
接下来我们还配置了一个 SpringSessionBackedSessionRegistry 实例,构建时传入了 session Repository。SpringSessionBackedSessionRegistry 继承自 SessionRegistry,用来维护会话信息注册表。
最后在 HttpSecurity 中配置 sessionRegistry 即可,相当于 spring-session 提供的 SpringSessionBackedSessionRegistry 接管了会话信息注册表的维护工作。
需要注意的是,引入了 spring-session 之后,不再需要配置 HttpSessionEventPublisher 实例,因为 spring-session 中通过 SessionRepositoryFilter 将请求对象重新封装为 SessionRepository RequestWrapper,并重写了 getSession 方法。在重写的 getSession 方法中,最终返回的是 HttpSessionWrapper 实例,而在 HttpSessionWrapper 定义时,就重写了 invalidate 方法。当调用会话的 invalidate 方法去销毁会话时,就会调用 RedisIndexedSessionRepository 中的方法,从 Redis 中移除对应的会话信息,所以不再需要 HttpSessionEventPublisher 实例。
最后再配置一个测试 Controller:
@RestController public class HelloController { @GetMapping("/") public String hello(HttpSession session) { return session.getClass().toString(); } }
在测试接口中返回 HttpSession 的类型以验证我们前面的讲解。
配置完成后,我们对项目进行打包,单击 IntelliJ IDEA 右侧的 Maven→Lifecycle→package 进行打包,如图 7-5 所示。
图 7-5 单击按钮对项目进行打包
打包完成后,进入 target 目录下,会有一个 jar,执行如下命令,分别启动两个实例:
java -jar sessionshare-0.0.1-SNAPSHOT.jar --server.port=8080 java -jar sessionshare-0.0.1-SNAPSHOT.jar --server.port=8081
两个实例启动完成后,这两个实例实际上共用了一个会话。接下来准备两个浏览器,先用浏览器 1 访问 8080 端口的项目,并完成登录操作;然后再用浏览器 2 访问 8081 端口的项目并完成登录操作。当浏览器 2 登录成功后,我们再去刷新浏览器 1,此时发现会话已经过期,说明集群环境下的会话管理已经生效。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论