1.简介
当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话(Session)浏览器在每次发送请求时都会携带一个SessionId,服务端则根据这个SessionId来判断用户身份。当浏览器关闭后,服务端的Session并不会自动销毁,需要开发者手动在服务端调用Session销毁方法,或者等Session过期时间到了自动销毁。在Spring Security中,与HttpSession相关的功能由SessionManagermentFilter和SessionAuthenticationStrategy接口来处理,SessionManagermentFilter过滤器将Session相关操作委托给SessionAuthenticationStrategy接口去完成。
2.会话并发管理
2.1.简介
会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一台设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多少台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在Spring Security中对此进行配置。
2.2.会话并发管理
@Configuration
public class SecurityConfig 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()用来开启会话管理、maximumSessions指定会话的并发数为1.
- HttpSessionEventPublisher提供一个个HttpSessionEventPublisher实例。Spring Security中通过一个Map集合来维护当前的HttpSession记录,进而实现会话的并发管理。当用户登录成功时,就向集合中添加一条HttpSession记录;当会话销毁时,就从结合中移除一条HttpSession记录。HttpSessionEventPublisher实现了HttpSessionListener接口,可以监听到HttpSession的创建和销毁时间,并将HttpSession的创建/销毁事件发布出去,这样,当由HttpSession销毁时,Spring Security就可以感知到该事件了。
2.3.测试会话管理
配置完成后,启动项目,使用谷歌浏览器进行登录,完成登录操作,之后使用火狐浏览器进行登录,完成登录操作,当第二个浏览器登录成功后,再回到第一个浏览器,刷新页面,结果发现无法正常访问。
3.传统Web开发处理
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement() //开启会话管理
.maximumSessions(1) //允许会话最大并发只能一个客户端
.expiredUrl("/login"); //当用户被挤下线之后跳转路径
}
4.前后端分离开发处理
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement() //开启会话管理
.maximumSessions(1) //允许会话最大并发只能一个客户端
// .expiredUrl("/login"); //当用户被挤下线之后跳转路径 传统Web开发
.expiredSessionStrategy(event -> { //前后端分离架构处理方案
HttpServletResponse response = event.getResponse();
Map<String,Object> result = new HashMap<>();
result.put("status",500);
result.put("msg","当前会话已经失效,请重新登录");
String s = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(s);
response.flushBuffer();
});
}
5.禁止再次登录
默认的效果是一种“被挤下线”的效果,后面登录的用户会把前面登录的用户“挤下线”。还有一种是禁止后来者登录,即一旦当前用户登陆成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录,配置如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement() //开启会话管理
.maximumSessions(1) //允许会话最大并发只能一个客户端
// .expiredUrl("/login"); //当用户被挤下线之后跳转路径 传统Web开发
.expiredSessionStrategy(event -> { //前后端分离架构处理方案
HttpServletResponse response = event.getResponse();
Map<String,Object> result = new HashMap<>();
result.put("status",500);
result.put("msg","当前会话已经失效,请重新登录");
String s = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(s);
response.flushBuffer();
})
.maxSessionsPreventsLogin(true); //一旦登录禁止再次登录
}
6.会话共享
前面所讲的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案就会失效。此时可以利用spring-session结合redis实现session共享。
6.1.实战
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
编写配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
配置springsecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final FindByIndexNameSessionRepository sessionRepository;
@Autowired
public SecurityConfig(FindByIndexNameSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement() //开启会话管理
.maximumSessions(1) //允许会话最大并发只能一个客户端
// .expiredUrl("/login"); //当用户被挤下线之后跳转路径 传统Web开发
.expiredSessionStrategy(event -> { //前后端分离架构处理方案
HttpServletResponse response = event.getResponse();
Map<String,Object> result = new HashMap<>();
result.put("status",500);
result.put("msg","当前会话已经失效,请重新登录");
String s = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(s);
response.flushBuffer();
})
.sessionRegistry(sessionRegistry())
.maxSessionsPreventsLogin(true);//一旦登录禁止再次登录
}
//创建 session 同步到redis 中方案
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry(){
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
}