所以这一切(不出所料)都是设计使然。这些spring security docs 很好地解释了正在发生的事情 - 引用:
在单个会话中接收并发请求的应用程序中,相同的SecurityContext 实例将在线程之间共享。即使使用了ThreadLocal,它也是从每个线程的HttpSession 检索到的同一个实例。如果您希望临时更改线程正在运行的上下文,这会产生影响。如果您只使用SecurityContextHolder.getContext(),并在返回的上下文对象上调用setAuthentication(anAuthentication),那么Authentication 对象将在共享相同SecurityContext 实例的所有并发线程中发生变化。您可以自定义SecurityContextPersistenceFilter 的行为,为每个请求创建一个全新的SecurityContext,防止一个线程中的更改影响另一个线程。或者,您可以在临时更改上下文的位置创建一个新实例。 SecurityContextHolder.createEmptyContext() 方法总是返回一个新的上下文实例。
上面引用的一个 sn-p 说:
...您可以自定义SpringContextPersistenceFilter的行为...
不幸的是,文档没有提供有关如何进行此操作或如何处理的任何信息。这个SO question 提出了这个问题(本质上是这个问题的提炼版本),但它没有受到太多关注。
还有这个SO answer 更深入地了解了HttpSessionSecurityContextRepository 的内部运作,这很可能是需要重写/更新才能解决此问题的部分。
如果我在实现中找到解决此问题的好方法(例如创建上下文的新实例),我将更新此答案。
更新
我遇到的问题的根源与从 HttpSession 中读取用户 ID 属性有关(在并发“注销”请求将其清除之后)。我没有实现自己的SpringContextRepository,而是决定创建一个简单的过滤器,将当前的Authentication 保存到请求中,然后从那里开始工作。
这是基本过滤器:
public class SaveAuthToRequestFilter extends OncePerRequestFilter {
public static final String REQUEST_ATTR = SaveAuthToRequestFilter.class.getCanonicalName();
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
throws ServletException, IOException {
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
request.setAttribute(REQUEST_ATTR, context.getAuthentication());
}
filterChain.doFilter(request, response);
}
}
必须在SecurityContextPersistenceFilter 之后添加,方法是将以下内容添加到您的WebSecurityConfigurerAdapter 的configure(final HttpSecurity http) 方法中。
http.addFilterAfter(new SaveAuthToRequestFilter(), SecurityContextPersistenceFilter.class)
完成之后,您可以从HttpServletRequest(@Autowired 或注入到您的控制器方法中)读取“当前”(每个线程/请求)Authentication 并从那里开始工作。我认为这个解决方案还有一些不足之处,但它是我能想到的最轻量级的选择。感谢@chimmi 和@sura2k 的启发。