SessionManagementFilter是在用户认证成功后,执行一些session相关的工作,包括防止固定会话攻击,多点登录登自动下线等。用户通过扩展点,能自定义各种策略。下面是SessionManagementFilter的流程图,分为4个子流程:
认证通过,执行策略成功,保存SecurityContext,执行下一个Filter
认证通过,执行策略失败,执行身份认证失败策略(一般是重定向到登录页面),结束请求
认证不通过,当前有session信息,并且session已经失效,执行session失效策略,结束请求
认证不通过,没有session信息,执行下一个Filter
在SessionManagementFilter的流程里支持三个扩展点,基本都是在SessionManagementConfigurer 里配置:
认证成功后执行的策略
认证失败后执行的策略
执行session失效后执行的策略
认证成功后执行的策略
在SessionManagementConfigurer 的getSessionAuthenticationStrategy() 方法获取SessionAuthenticationStrategy ,返回的是CompositeSessionAuthenticationStrategy 对象。从名字上可以看出来,这是一个组合对象,包含一组策略,具体包含逻辑如下:
第一步,用户指定一个验证策略,或者使用默认策略
指定方式,http.sessionManagement(it -> it.sessionAuthenticationStrategy(null))
默认策略ChangeSessionIdAuthenticationStrategy ,也就是登录成功后会换一个SessionId,防止固定会话攻击。也可以通过http.sessionManagement(it -> it.sessionFixation(z -> z.migrateSession())) 配置来更换其他策略。
第二步,如果开启了最大用户回话数的限制,会额外添加2个策略
ConcurrentSessionControlAuthenticationStrategy 将超过限制的session设置为过期状态
RegisterSessionAuthenticationStrategy 把当前登录人信息记录到session上,默认是记录在内存里。可以通过SpringSession接入到Redis。
第三步,如果开启了CSRF验证,用户可以额外添加一个验证策略,或者使用默认CSRF策略
指定方式,http.csrf(it -> it.sessionAuthenticationStrategy(null))
默认策略CsrfAuthenticationStrategy ,重新设置一个新的CSRF token
认证失败后执行的策略
在ConcurrentSessionControlAuthenticationStrategy 策略时,如果用户登录的session数量超过了限制,并且设置要抛出异常(http.sessionManagement(it -> it.sessionConcurrency(sc -> sc.maxSessionsPreventsLogin(true)))),那么就会抛出SessionAuthenticationException ,从而触发认证失败策略的执行。
默认用的是SimpleUrlAuthenticationFailureHandler ,会重定向到指定页面。
执行session失效后执行的策略
这里也是配合ConcurrentSessionControlAuthenticationStrategy 策略,用户登录的session数量超过了限制时,当前session被设置了失效后,要执行的操作。默认没有执行策略,设置方法如下:
直接设置invalidSessionStrategy,http.sessionManagement(it -> it.invalidSessionStrategy(null)
设置invalidSessionUrl就会创建SimpleRedirectInvalidSessionStrategy,http.sessionManagement(it -> it.invalidSessionUrl("xx")
分布式环境下,控制用户登录session数量
关于ConcurrentSessionControlAuthenticationStrategy策略,默认情况,session是维护在内存里,所以在分布式部署模式下,就没法拿到全局的登录数。这就要调整session保存方式,我们要实现自己的SessionRegistry 。
SpringSecurity预留了扩展类SpringSessionBackedSessionRegistry ,配合SpringSession可以很方便将session存入DB、Redis等。以存入Redis为例,只需要添加几个配置:
引入spring-session-data-redis和spring-boot-starter-data-redis
<!-- 对接spring session -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- 自动配置redis客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
开启@EnableRedisIndexedHttpSession 注解
@EnableRedisIndexedHttpSession
@AutoConfiguration
public class AutoConfiguration {
}
使用SpringSessionBackedSessionRegistry 替代默认实现,并设置maximumSessions最大并发数
@Configuration
public class SecurityConfiguration {
@Autowired
FindByIndexNameSessionRepository sessionRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(it -> it.sessionConcurrency(sc -> sc
.sessionRegistry(new SpringSessionBackedSessionRegistry<>(sessionRepository))
.expiredSessionStrategy(event -> {
log.info("expiredSessionStrategy {}", event.getSessionInformation());
ResponseHelper.commitHttpResponse("您已经退出,多设备登录", HttpStatus.FORBIDDEN.value(), event.getResponse());
})
.maximumSessions(1)))
// 略...
}
}