一、前言
此篇是对上篇 Spring Security 6.1.x 系列(5)—— Servlet 认证体系结构介绍 中4.9章节显式调用SecurityContextRepository#saveContext
进行详解分析。
二、设置和修改登录态
2.1 登录态存储形式
使用Spring Security
框架,认证成功后的用户信息会放在Authentication
对象的Principal
中,
Authentication
对象又会被放入SecurityContext
中,而SecurityContext
存在这2个地方:
-
SecurityContextHolderStrategy
:线程级别的SecurityContext
持有策略。有全局共享、线程继承、线程隔离等几种获取上下文的方式(上文有过介绍)。 -
SecurityContextRepository
:持久化SecurityContext
,默认存入HttpServletRequest
和HttpSession
。
在代码中,我们要获取用户登录信息,可以通过SecurityContextHolder.getContext().getAuthentication()
的方式获取,这种方式是从SecurityContextHolderStrategy
获取用户数据。而SecurityContextHolderStrategy
初始化数据又是来自SecurityContextRepository
,相关逻辑是在SecurityContextHolderFilter
类里。
SecurityContextHolderFilter#doFilter
源码如下:
RequestAttributeSecurityContextRepository
源码如下:
设想一种场景,在用户登录后,编辑了用户信息,这时要同步刷新SecurityContext
里的用户信息。我们要如何更新这两个处的用户数据呢?
2.2 更新SecurityContextHolderStrategy中的用户信息
要更新SecurityContextHolderStrategy
非常简单,因为它保存在内存里,只要通过SecurityContextHolder.getContext().getAuthentication()
获取认证信息后,直接设置对应的属性,内存中属性值发生变化,后续处理逻辑就能读到最新值。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
MyUser myUser = (MyUser) authentication.getPrincipal();
myUser.setNickname("新的昵称");
2.3 更新SecurityContextRepository中的用户信息
2.3.1 无法直接获取SecurityContextRepository对象
要知道怎么更新SecurityContextRepository
,我们先看其他地方是怎么调用它。往SecurityContextRepository
写数据是在用户认证成功之后,调用AbstractAuthenticationProcessingFilter#successfulAuthentication()
方法,执行认证成功的后续逻辑时。
这里的this.securityContextRepository
是通过setter
方法传进来的,并且发现Spring Security
没有把SecurityContextRepository
注册到Spring
容器,而且Spring Security
其他持有SecurityContextRepository
对象的类都没有暴露SecurityContextRepository
的获取方法。也就是说,我们无法从Spring Security
拿到默认的SecurityContextRepository
对象。
在AbstractAuthenticationProcessingFilter
中,securityContextRepository
的类型是 RequestAttributeSecurityContextRepository
,这个表示将SecurityContext
存入到当前请求的属性中,那很明显,在当前请求结束之后,这个数据就没了。
2.3.2 手动设置SecurityContextRepository对象
在Spring Security
的自动化配置类中,将securityContextRepository
属性指向了 DelegatingSecurityContextRepository
,这是一个代理的存储器,代理的对象是 RequestAttributeSecurityContextRepository
和 HttpSessionSecurityContextRepository
,所以在默认的情况下,用户登录成功之后,在这里就把登录用户数据存入到HttpSessionSecurityContextRepository
中了。
部分DelegatingSecurityContextRepository
源码如下:
为了顺利拿到SecurityContextRepository
对象,我们可以手动往Spring
容器注册一个SecurityContextRepository
对象,然后把它塞到Spring Security
里。通过这种方式,我们能从Spring
容器拿到SecurityContextRepository
对象,然后随时刷新SecurityContext
。
具体实现代码如下:
@Bean
public SecurityContextRepository securityContextRepository() {
return new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, SecurityContextRepository securityContextRepository) throws Exception {
httpSecurity
.securityContext((context) -> context
.securityContextRepository(securityContextRepository())
);
}
这里DelegatingSecurityContextRepository
是Spring Security
的默认值,我们原封不动保留下来。
2.3.3 更新登录态
在更新完SecurityContextHolderStrategy
对象之后,我们把显示SecurityContext
重新保存到SecurityContextRepository
。
// 更新SecurityContextHolderStrategy
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
MyUser myUser = (MyUser) authentication.getPrincipal();
myUser.setNickname("新的昵称");
// 更新SecurityContextRepository
securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
三、总结
通过显式更新SecurityContextHolderStrategy
、SecurityContextRepository
,我们就能完整更新SecurityContext
中的用户信息。如果项目中引入了Spring Session
,Spring Session
维护的登录态也会同步更新。