简介
当浏览器调用登录接口登录成功之后,服务端会和浏览器之间创建一个会话(Session),浏览器在每次发送请求时都会携带一个 SessionId,服务端则根据这个 SessionId 来判断用户身份。当浏览器关闭之后,服务端的 Session 并不会自动销毁,需要开发者手动在服务端调用 Session 销毁方法,或者等 Session 过期时间到了自动销毁。在 Spring Security 中,与 HttpSession 相关的功能由 SessionManagementFilter 和 SessionAuthenticationStrategy 接口来处理,Session 相关操作委托给 SessionAuthenticationStrategy 接口去完成
简介
会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一个设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在 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();
}
}
传统 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();
});
}
默认的效果是一种被 “挤下线” 的效果,后面登录的用户会把前面登录的用户 “挤下线”。还有一种禁止后来者登录,即一旦当前用户登陆成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录,配置如下
@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); // 一旦登录,禁止再次登录
}
前面所有的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案就会失效。此时可以利用 spring-session 结合 redis 实现 session 共享
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
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
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
}
}
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 {
}