SpringSecurity 登录成功后获取不到用户信息的问题
问题复现
按照正常的登录逻辑 登录之后返回token 并且在SpringSecurity中存入认证过的Authentication对象 然后我携带token访问一个资源的时候 在过滤器中发现
SecurityContextHolder.getContext().getAuthentication()==null
但是通过了过滤器之后 在controller中又能正常的取到认证的后的Authentication对象 。
解释
参考了江南一点雨的博客
SecurityContextHolder 中的数据,本质上是保存在
ThreadLocal
中,ThreadLocal
的特点是存在它里边的数据,哪个线程存的,哪个线程才能访问到。这样就带来一个问题,当不同的请求进入到服务端之后,由不同的 thread 去处理,按理说后面的请求就可能无法获取到登录请求的线程存入的数据,例如登录请求在线程 A 中将登录用户信息存入
ThreadLocal
,后面的请求来了,在线程 B 中处理,那此时就无法获取到用户的登录信息。但实际上,正常情况下,我们每次都能够获取到登录用户信息,这又是怎么回事呢?
这我们就要引入 Spring Security 中的
SecurityContextPersistenceFilter
了。无论Shiro还是SpringSecurity 都是一系列的过滤器 UsernamePasswordAuthenticationFilter 过滤器,在这个过滤器之前,还有一个过滤器就是 SecurityContextPersistenceFilter,请求在到达 UsernamePasswordAuthenticationFilter 之前都会先经过 SecurityContextPersistenceFilter。
原本的方法很长,我这里列出来了比较关键的几个部分:
-
SecurityContextPersistenceFilter 继承自 GenericFilterBean,而 GenericFilterBean 则是 Filter 的实现,所以
-
SecurityContextPersistenceFilter 作为一个过滤器,它里边最重要的方法就是 doFilter 了。
-
在 doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到 readSecurityContextFromSession 方法中,在这里我们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是 SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。
-
SecurityContext 是一个接口,它有一个唯一的实现类 SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。
-
在拿到 SecurityContext 之后,通过 SecurityContextHolder.setContext 方法将这个 SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从 SecurityContextHolder 中获取到用户信息了。
-
接下来,通过 chain.doFilter 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter 过滤器中了)。
在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从 SecurityContextHolder 中获取到 SecurityContext,获取到之后,会把 SecurityContextHolder 清空,然后调用 repo.saveContext 方法将获取到的 SecurityContext 存入 session 中。
至此,整个流程就很明了了。
个人的理解如下
要想从当前的session取到上一次登录过存入的authentication对象 首先要经过一系列的过滤器进行初始化 所以在这些过滤器之前 通过
SecurityContextHolder.getContext().getAuthentication() 来获取 相当于新开了一个线程取 此时还没有进行初始化 也就取不到,
所以在JWT token的过滤器中 如果已经确定了token没有问题 的话 我们自己创建一个已经认证的Authentication放入上下文中 这里使用的是UsernamePasswordAuthenticationToken
构造方法如下
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
{
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
于是也就有了下面的 jwtFilter 的代码
if (StrUtil.isNotEmpty(loginAccountUsername)&&SecurityContextHolder.getContext().getAuthentication()==null)
{
UserDetails userDetails=adminService.loadUserByUsername(loginAccountUsername);
if (userDetails!=null&&jwtTokenUtil.validateToken(authToken,userDetails))
{
UsernamePasswordAuthenticationToken authentication=new UsernamePasswordAuthenticationToken(userDetails,userDetails.getPassword(),userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("token正确 自动登录成功"+userDetails.getUsername());
}
}