2.2 会话管理

2.2.1 会话超时设置

image.png

最短有效期为60秒(60s),默认为30分钟(30m) 会话超时后,再次请求系统会跳转到登录页面

2.2.2 会话并发控制

策略介绍

会话并发控制可以控制相同用户最多可以在多少个不同设备上登录。
常见策略:

  • 一个账号同一时间段只允许在一个设备登录

    • 策略一:第二个设备登录,不允许登录,提示账号已经在其它设备登录。
    • 策略二:第二个设备登录,第二个设备强制踢出;

      重写 hashCode() 和 equals() 方法

      要判断登录的用户是同一个用户,两个 UserDetails 是同一个对象就可以判断,所以我们需要重新 UserDetails 的实现类 User 类的 hashCode() 和 equals() 方法:
      image.png

      禁用多设备登录

      配置

      image.png

      测试

      使用一款浏览器先登录,再使用另一款浏览器用相同帐号登录,发现第二个浏览器无法登录,控制台抛出如下异常:

      1. org.springframework.security.web.authentication.session.SessionAuthenticationException:
      2. Maximum sessions of 1 for this principal exceeded

      处理异常

      image.png

      再次测试

      image.png

      踢出上一个设备的用户

      配置

      image.png

      测试

      使用一款浏览器先登录,再使用另一款浏览器用相同帐号登录,两个浏览器都能登录成功,第二个浏览器登录成功后,刷新第一个浏览器,提示如下:
      image.png
      控制台并没有抛出异常信息,我们需要自定义会话失效的策略。

      自定义并发会话失效策略

      需要实现 SessionInformationExpiredStrategy 接口并配置,代码如下:
      image.png

      @Override
      protected void configure(HttpSecurity http) throws Exception {
         ....
      
         .and()
         // 会话管理
         .sessionManagement()
         .maximumSessions(1) // 设置一个账号同时登录的设备数
         // 超过最大登录数的策略
         // true:不允许再登录新设备
         // false:踢出上一个设备的会话 默认false
         .maxSessionsPreventsLogin(false)
         .expiredSessionStrategy(event -> {
             Map<String, String> result = new HashMap<>();
             result.put("code", "403");
             result.put("msg", "您的账号已经在其它设备登录,如非本人操作,请立即修改密码!");
             HttpServletResponse response = event.getResponse();
             response.setStatus(HttpStatus.UNAUTHORIZED.value());
             response.setContentType("application/json;charset=UTF-8");
             response.getWriter().write(objectMapper.writeValueAsString(result));
         })
         .and()
      
         ....
      }
      

      再次测试

      image.png

      2.2.3 Session 共享

      介绍

      在传统的单服务架构中,一般来说,只有一个服务器,那么不存在 Session 共享问题,但是在分布式/集群项目中,Session 共享则是一个必须面对的问题,先看一个简单的架构图:
      image.png
      在这样的架构中,会出现一些单服务中不存在的问题,例如客户端发起一个请求,这个请求到达 Nginx 上之后,被 Nginx 转发到 Tomcat A 上,然后在 Tomcat A 上往 Session 中保存了一份数据,下次又来一个请求,这个请求被转发到 Tomcat B 上,此时再去 Session 中获取数据,发现没有之前的数据。
      对于这一类问题的解决,目前比较主流的方案就是将各个服务之间需要共享的数据,保存到一个公共的地方(主流方案就是 Redis):
      image.png
      当所有 Tomcat 需要往 Session 中写数据时,都往 Redis 中写,当所有 Tomcat 需要读数据时,都从 Redis 中读。这样,不同的服务就可以使用相同的 Session 数据了。
      Spring Session 可以很方便的实现该功能,Spring Session 就是使用 Spring 中的代理过滤器,将所有的 Session 操作拦截下来,自动的将数据同步到 Redis 中,或者自动的从 Redis 中读取数据。
      对于开发者来说,所有关于 Session 同步的操作都是透明的,开发者使用 Spring Session,一旦配置完成后,具体的用法就像使用一个普通的 Session 一样。

      Session 信息保存到 Redis

      引入 spring-boot-starter-data-redis 和 spring-session-data-redis 的依赖。
      spring-session 是 spring 框架提供的分布式会话模块,可以很轻松的实现 session 共享。 ```xml

      org.springframework.boot spring-boot-starter-data-redis

org.springframework.session spring-session-data-redis

Redis 配置:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1429839/1616906555387-e3fcec03-7059-4911-bdc2-0fbae34d8391.png#align=left&display=inline&height=73&margin=%5Bobject%20Object%5D&name=image.png&originHeight=73&originWidth=336&size=7156&status=done&style=none&width=336)<br />启动应用登录测试,查看 Redis 数据库是否存入 Session 相关信息。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1429839/1616906985999-e06e2f86-16b5-4c57-8a38-76dfaed45b82.png#align=left&display=inline&height=261&margin=%5Bobject%20Object%5D&name=image.png&originHeight=261&originWidth=772&size=26836&status=done&style=none&width=772)<br />录入系统后,查看 Redis 数据库:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1429839/1616907171338-a4d721a6-561a-4ff1-932a-ccfd6a64f3ba.png#align=left&display=inline&height=134&margin=%5Bobject%20Object%5D&name=image.png&originHeight=134&originWidth=981&size=15995&status=done&style=none&width=981)<br />Session 会话信息已经存入 Redis。
<a name="RNM5r"></a>
### 集群测试
<a name="jDmvD"></a>
#### 代码
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1429839/1616907709400-b5a33c4b-a44e-41f7-958e-1361d8c846a2.png#align=left&display=inline&height=181&margin=%5Bobject%20Object%5D&name=image.png&originHeight=181&originWidth=303&size=5672&status=done&style=none&width=303)<br />在该控制器添加输出用户信息和应用端口号的方法:
```java
@Value("${server.port}")
private int port;

@RequestMapping("/sysInfo")
@ResponseBody
public Map<String, Object> sysInfo(@AuthenticationPrincipal User user) {
    Map<String, Object> result = new HashMap<>();
    result.put("port", port);
    result.put("user", user);
    return result;
}

打包应用

打包时候跳过测试,在 pom.xml 中添加如下插件:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.22.2</version>
  <configuration>
    <skipTests>true</skipTests>
  </configuration>
</plugin>

image.png
打包后的jar:

image.png
启动应用

java -jar springsecurity-basic-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar springsecurity-basic-0.0.1-SNAPSHOT.jar --server.port=8081

从 8080 应用登录系统:
image.png
查看系统信息:
image.png
直接访问 8081 应用的系统信息,无需再次登录就可以看到信息,Session 共享成功。
image.png
在任意一个系统注销用户,另一个系统也会被踢回到登录页面。

配置集群

image.png
nginx.conf 内容:

worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    upstream server { 
                server localhost:8080;
                server localhost:8081;
    }
    server {
        listen    80;                        # nginx HTTP服务端口号
        location / {
            proxy_pass http://server;      # 请求转向 server 定义的服务器列表
        }
    }
}

启动nginx:
image.png
访问:http://localhost 登录成功以后,每次刷新主页
image.png
image.png
8080 和 8081 应用的页面会轮询显示,说明两个应用的 session 会话已经共享成功。