一、前言
此篇是对上篇 Spring Security 6.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
源码如下:
查看securityContextRepository
如下:
设想一种场景,在用户登录后,编辑了用户信息,这时要同步刷新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
是一个接口,源码如下:
public interface SecurityContextRepository {/** @deprecated */@DeprecatedSecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);default DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {Supplier<SecurityContext> supplier = () -> {return this.loadContext(new HttpRequestResponseHolder(request, (HttpServletResponse)null));};return new SupplierDeferredSecurityContext(SingletonSupplier.of(supplier), SecurityContextHolder.getContextHolderStrategy());}void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);boolean containsContext(HttpServletRequest request);
}
SecurityContextRepository
接口有以下几个实现类:
- NullSecurityContextRepository:什么都不做,也就是不会存储,每次请求都需要重新认证
- HttpSessionSecurityContextRepository:将
SecurityContext
保存在Session
中,获取的时候,从Session
中查询 - RequestAttributeSecurityContextRepository:将
SecurityContext
保存在请求对象HttpServletRequest
中 - DelegatingSecurityContextRepository:委派代理存储,内部维护多个
SecurityContextRepository
,实现同时支持多种方式存储SecurityContext
。
2.3.2 无法直接获取SecurityContextRepository对象
要知道怎么更新SecurityContextRepository
,我们先看其他地方是怎么调用它。往SecurityContextRepository
写数据是在用户认证成功之后,调用AbstractAuthenticationProcessingFilter#successfulAuthentication()
方法,执行认证成功的后续逻辑时。
这里的this.securityContextRepository
是通过setter
方法传进来的,并且发现Spring Security
没有把SecurityContextRepository
注册到Spring
容器,而且Spring Security
其他持有SecurityContextRepository
对象的类都没有暴露SecurityContextRepository
的获取方法。也就是说,我们无法从Spring Security
拿到默认的SecurityContextRepository
对象。
debug
发现this.securityContextRepository
属性指向了 DelegatingSecurityContextRepository
,这是委派代理存储,代理的对象是 RequestAttributeSecurityContextRepository
和 HttpSessionSecurityContextRepository
,所以在默认的情况下,用户登录成功之后,在这里就把登录用户数据分别存入到HttpSessionSecurityContextRepository
和RequestAttributeSecurityContextRepository
中。
部分DelegatingSecurityContextRepository
源码如下:
2.3.3 手动设置SecurityContextRepository对象
为了顺利拿到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
中的SecurityContextRepository
默认实现,我们原封不动保留下来。
2.3.4 更新登录态
在更新完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
维护的登录态也会同步更新。