八、会话管理
简介
当浏览器调用登录接口登录成功之后,服务端会和浏览器之间创建一个会话(Session),浏览器在每次发送请求时都会携带一个 SessionId,服务端则根据这个 SessionId 来判断用户身份。当浏览器关闭之后,服务端的 Session 并不会自动销毁,需要开发者手动在服务端调用 Session 销毁方法,或者等 Session 过期时间到了自动销毁。在 Spring Security 中,与 HttpSession 相关的功能由 SessionManagementFilter 和 SessionAuthenticationStrategy 接口来处理,Session 相关操作委托给 SessionAuthenticationStrategy 接口去完成
8.1 会话并发管理
简介
会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一个设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在 SpringSecurity 中对此进行配置
- 代码配置
package com.vinjcent.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement() // 开启会话管理
.maximumSessions(1); // 允许会话最大并发只能一个客户端
}
// 用于监听会话的创建和销毁
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
- 使用两个浏览器进行登录,观察一个登录之后,在进行另外一个登录,前者再次访问系统资源将会被提醒该用户正在被使用
- sessionManagement() 用来开启会话管理、sessionManagement() 用来指定会话的并发数
- HttpSessionEventPublisher 提供一个 HttpSessionEventPublisher 实例。SpringSecurity 中通过一个 Map 集合来维护当前的 HttpSession 记录,进而实现会话的并发管理。当用户登陆成功时,就向集合中添加一条 HttpSession 记录;当会话销毁时,就从集合中移除一条 HttpSession 记录。HttpSessionEventPublisher 实现了 HttpSessionListener 接口,可以监听到 HttpSession 的创建和销毁事件,并将 HttpSession 的 创建/销毁 事件发布出去,这样,当有 HttpSession 销毁时,SpringSecurity 就可以感知到该事件了
8.2 会话失效
传统 web 开发处理
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
...
.sessionManagement() // 开启会话管理
.maximumSessions(1) // 允许会话最大并发只能一个客户端
.expiredUrl("/toLogin"); // 会话过期处理
}
前后端分离
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement() // 开启会话管理
.maximumSessions(1) // 允许会话最大并发只能一个客户端
// .expiredUrl("/toLogin"); // 传统架构的会话过期处理方案
.expiredSessionStrategy(event -> { // 前后端分离架构会话过期处理方案
HttpServletResponse response = event.getResponse();
HashMap<String, Object> result = new HashMap<>();
result.put("status", 500);
result.put("msg", "当前会话已经失效,请重新登录");
String str = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(str);
response.flushBuffer();
});
}
- 测试访问
8.3 禁止再次登录
默认的效果是一种被 “挤下线” 的效果,后面登录的用户会把前面登录的用户 “挤下线”。还有一种禁止后来者登录,即一旦当前用户登陆成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录,配置如下
- 代码配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout() // 需要开启退出登录
.and()
.csrf()
.disable()
.sessionManagement() // 开启会话管理
.maximumSessions(1)
// .expiredUrl("/toLogin")
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
HashMap<String, Object> result = new HashMap<>();
result.put("status", 500);
result.put("msg", "当前会话已经失效,请重新登录");
String str = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(str);
response.flushBuffer();
})
.maxSessionsPreventsLogin(true); // 一旦登录,禁止再次登录
}
8.4 会话共享
前面所有的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案就会失效。此时可以利用 spring-session 结合 redis 实现 session 共享
- 实战
- 引入依赖
pom.xml
- 引入依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--session-redis-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 配置
application.yml
文件
# 端口号
server:
port: 3035
servlet:
session:
# 设置session过期时间
timeout: 1
# 服务应用名称
spring:
application:
name: SpringSecurity10security
# The Redis settings
redis:
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# 日志处理,为了展示 mybatis 运行 sql 语句
logging:
level:
com:
vinjcent:
debug
- 配置 Security 适配器
- WebSecurityConfigurerAdapter
package com.vinjcent.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
// 注入 session 仓库
private final FindByIndexNameSessionRepository sessionRepository;
@Autowired
public WebSecurityConfiguration(FindByIndexNameSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
// 注册 session 同步到 redis 中
@Bean
public SessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.successForwardUrl("/test")
.and()
.logout() // 需要开启退出登录
.and()
.csrf()
.disable()
.sessionManagement() // 开启会话管理
.maximumSessions(1)
// .expiredUrl("/toLogin");
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
HashMap<String, Object> result = new HashMap<>();
result.put("status", 500);
result.put("msg", "当前会话已经失效,请重新登录");
String str = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(str);
})
.maxSessionsPreventsLogin(true) // 一旦登录,禁止再次登录
.sessionRegistry(sessionRegistry()); // 将 session 交给谁管理,前后端分离自定义过滤器需要配setSessionAuthenticationStrategy
}
}
- 将 redis 作为全局 HttpSession(不开启redis作为HttpSession会使得前面的配置失效,导致出现每个服务有各自的 session 情况)
package com.vinjcent.config.redis;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession // 将整个应用中使用session的数据全部交给redis处理
public class RedisSessionManager {
}
- 模拟分布式服务
- 将当前的服务复制一份运行,并设置运行vm参数
- 启动本地 redis-server 服务,测试两个服务进行登录操作,结果如图所示