返回介绍

7.4 Session 共享

发布于 2025-04-26 13:16:46 字数 5268 浏览 0 评论 0 收藏

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。