spring security 会话管理
一、简介
当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话(Session)浏览器在每次发送请求时都会携带一个 Sessionld,服务端则根据这个 Sessionld 来判断用户身份当浏览器关闭后,服务端的 Session 并不会自动销毁,需要开发者手动在服务端调用Session销毁方法,或者等 Session 过期时间到了自动销毁。在Spring Security 中,与HttpSession相关的功能由 SessionManagemenFiter 和Session AutheaticationStrateey 接口来处理,SessionManagomentFilter 过滤器将Session 相关操作委托给SessionAuthenticationStrategy 接口去完成。
二、会话并发管理
会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一合设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多少台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在 Spring Security 中对此进行配置。
2.1 开启会话管理
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义数据源,从内存中,后期自己写一个mybatis 从数据库查询
* @throws Exception{scrypt}
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("admin").password("{noop}12345").authorities("admin").build());
return userDetailsManager;
}
/**
* 自定义authenticationManager 管理器,将自定义的数据源加到其中
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()// 表单登录
.loginProcessingUrl("/doLogin")
.and()
.csrf().disable()
.sessionManagement()// 开启会管理
.maximumSessions(1); //设置会话并发数为1
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
- sessionManagement)用来开启会话管理、maximumSessions指定会话的并发数为1。
- HttpSessionEventPublisher提供一一个Htp SessionEvenePubishor-实例。Spring Security中通过一个 Map 集合来集护当前的 Http Session 记录,进而实现会话的并发管理。当用户登录成功时,就向集合中添加一条Http Session 记录;当会话销毁时,就从集合中移除一条Httpsession 记录。HtpSesionEvenPublisher 实现了 Fttp SessionListener 接口,可以监听到 HtpSession 的创建和销毁事件,并将 Fltp Session 的创建/销毁事件发布出去,这样,当有 HttpSession 销毁时,Spring Security 就可以感知到该事件了。
2.2 测试会话管理
配置完成后,启动项目。这次测试我们需要两个浏览器,如果使用了 Chrome 浏览器,可以使用 Chrome浏览器中的多用户方式(相当于两个浏览器)先在第一个浏览器中输入 http://localhost:8081,此时会自动跳转到登录页面,完成登录操作,就可以访问到数据了;接下来在第二个浏览器中也输入 http://localhost:8081,也需要登录完成登录操作;当第二个浏览器登录成功后,再回到第一个浏览器,刷新页面。结果出现下当另外一个浏览器登录后,就把前一个会话挤掉线了;
2.3 会话失效处理(传统web)
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()// 表单登录
.loginProcessingUrl("/doLogin")
.and()
.csrf().disable()
.sessionManagement()// 开启会管理
.maximumSessions(1) //设置会话并发数为1
.expiredUrl("/login"); // 会话过期跳转页面
}
这次会话失效后,就会跳到登录页面了,就不会出现这个英文提示了;
2.4 会话失效处理(前后端分离实现)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()// 表单登录
.loginProcessingUrl("/doLogin")
.and()
.csrf().disable()
.sessionManagement()// 开启会管理
.maximumSessions(1) //设置会话并发数为1
//.expiredUrl("/login"); // 会话过期跳转页面
.expiredSessionStrategy(ses -> {
HttpServletResponse response = ses.getResponse();
Map<String,String> resMap = new HashMap<>();
resMap.put("code","5001");
resMap.put("msg","你的账号已经挤下线了,请重新登录!");
String resStr = new ObjectMapper().writeValueAsString(resMap);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(resStr);
});
}
2.5 禁止再此登录
默认的效果是一种被“挤下线”的效果,后面登录的用户会把前面登录的用户“挤下线。还有一种是禁止后来者登录,即一旦当前用户登录成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录,配置如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()// 表单登录
.loginProcessingUrl("/doLogin")// 退出登录
.and()
.logout()
.and()
.csrf().disable()
.sessionManagement()// 开启会管理
.maximumSessions(1) //设置会话并发数为1
//.expiredUrl("/login"); // 会话过期跳转页面
.expiredSessionStrategy(ses -> {
HttpServletResponse response = ses.getResponse();
Map<String,String> resMap = new HashMap<>();
resMap.put("code","5001");
resMap.put("msg","你的账号已经挤下线了,请重新登录!");
String resStr = new ObjectMapper().writeValueAsString(resMap);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(resStr);
response.flushBuffer();
}).maxSessionsPreventsLogin(true);// 一旦登录 禁止后来其它账号登录
}
三、会话共享
前面所讲的会话管理都是单机上的会话管理,也就是在单体架构,如果当前是集群环境,前面所讲的会话管理方案就会失效。此时可以利用 spring-session 结合 redis 实现 session 共享。
3.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>
3.2 代码配置
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()// 表单登录
.loginProcessingUrl("/doLogin")// 退出登录
.and()
.logout()
.and()
.csrf().disable()
.sessionManagement()// 开启会管理
.maximumSessions(1) //设置会话并发数为1
//.expiredUrl("/login"); // 会话过期跳转页面
.expiredSessionStrategy(ses -> {
HttpServletResponse response = ses.getResponse();
Map<String,String> resMap = new HashMap<>();
resMap.put("code","5001");
resMap.put("msg","你的账号已经挤下线了,请重新登录!");
String resStr = new ObjectMapper().writeValueAsString(resMap);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(resStr);
response.flushBuffer();
})
//.maxSessionsPreventsLogin(true);// 一旦登录 禁止后来其它账号登录
.sessionRegistry(sessionRegistry());// 将session 交给谁管理
}
// 创建session 同步到redis中方案
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry() {
FindByIndexNameSessionRepository indexNameSessionRepository = new RedisIndexedSessionRepository(redisTemplate);
return new SpringSessionBackedSessionRegistry(indexNameSessionRepository);
}
最终查看redis结果