一开始我觉得这可能是一个小概率 BUG,但是当问的人多了,我觉得这个问题对于新手来说还有一定的普遍性,有必要来写篇文章跟大家仔细聊一聊这个问题,防止小伙伴们掉坑。
http://mpvideo.qpic.cn/0b78xiaaiaaaimakvjmbpzpfbowdas5aabaa.f10002.mp4?dis_k=174e4b541976b459a1983850a17d6fa9&dis_t=1586485687
视频看完了,如果小伙伴们觉得松哥的视频风格还能接受,也可以看看松哥自制的 Spring Boot + Vue 系列视频教程
以下是视频笔记。
如果使用了 Spring Security,当我们登录成功后,可以通过如下方式获取到当前登录用户信息:
这两种办法,都可以获取到当前登录用户信息。具体的操作办法,大家可以看看松哥之前发布的教程:Spring Security 如何动态更新已登录用户信息?。
正常情况下,我们通过如上两种方式的任意一种就可以获取到已经登录的用户信息。
异常情况,就是这两种方式中的任意一种,都返回 null。
都返回 null,意味着系统收到当前请求时并不知道你已经登录了(因为你没有在系统中留下任何有效信息),这会带来两个问题:
要弄明白这个问题,我们就得明白 Spring Security 中的用户信息到底是在哪里存的?
前面说了两种数据获取方式,但是这两种数据获取方式,获取到的数据又是从哪里来的?
首先松哥之前和大家聊过,SecurityContextHolder 中的数据,本质上是保存在 ThreadLocal
中,ThreadLocal
的特点是存在它里边的数据,哪个线程存的,哪个线程才能访问到。
这样就带来一个问题,当不同的请求进入到服务端之后,由不同的 thread 去处理,按理说后面的请求就可能无法获取到登录请求的线程存入的数据,例如登录请求在线程 A 中将登录用户信息存入 ThreadLocal
,后面的请求来了,在线程 B 中处理,那此时就无法获取到用户的登录信息。
但实际上,正常情况下,我们每次都能够获取到登录用户信息,这又是怎么回事呢?
这我们就要引入 Spring Security 中的 SecurityContextPersistenceFilter
了。
小伙伴们都知道,无论是 Spring Security 还是 Shiro,它的一系列功能其实都是由过滤器来完成的,在 Spring Security 中,松哥前面跟大家聊了 UsernamePasswordAuthenticationFilter
过滤器,在这个过滤器之前,还有一个过滤器就是 SecurityContextPersistenceFilter
,请求在到达 UsernamePasswordAuthenticationFilter
之前都会先经过 SecurityContextPersistenceFilter
。
我们来看下它的源码(部分):
public class SecurityContextPersistenceFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
}
}
}
原本的方法很长,我这里列出来了比较关键的几个部分:
Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
,这里的 springSecurityContextKey 对象的值就是 SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。UsernamePasswordAuthenticationFilter
过滤器中了)。至此,整个流程就很明了了。
每一个请求到达服务端的时候,首先从 session 中找出来 SecurityContext ,然后设置到 SecurityContextHolder 中去,方便后续使用,当这个请求离开的时候,SecurityContextHolder 会被清空,SecurityContext 会被放回 session 中,方便下一个请求来的时候获取。
搞明白这一点之后,再去解决 Spring Security 登录后无法获取到当前登录用户这个问题,就非常 easy 了。
经过上面的分析之后,我们再来回顾一下为什么会发生登录之后无法获取到当前用户信息这样的事情?
最简单情况的就是你在一个新的线程中去执行 SecurityContextHolder.getContext().getAuthentication()
,这肯定获取不到用户信息,无需多说。例如下面这样:
@GetMapping("/menu")
public List<Menu> getMenusByHrId() {
new Thread(new Runnable() {
@Override
public void run() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication);
}
}).start();
return menuService.getMenusByHrId();
}
这种简单的问题相信大家都能够很容易排查到。
还有一种隐藏比较深的就是在 SecurityContextPersistenceFilter 的 doFilter 方法中没能从 session 中加载到用户信息,进而导致 SecurityContextHolder 里边空空如也。
在 SecurityContextPersistenceFilter 中没能加载到用户信息,原因可能就比较多了,例如:
什么时候会发生这个问题呢?有的小伙伴可能在配置 SecurityConfig#configure(WebSecurity) 方法时,会忽略掉一个重要的点。
当我们想让 Spring Security 中的资源可以匿名访问时,我们有两种办法:
这两种办法对应了两种不同的配置方式。其中第一种配置可能会影响到我们获取登录用户信息,第二种则不影响,所以这里我们来重点看看第一种。
不想走 Spring Security 过滤器链,我们一般可以通过如下方式配置:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico","/verifyCode");
}
正常这样配置是没有问题的。
如果你很不巧,把登录请求地址放进来了,那就 gg 了。虽然登录请求可以被所有人访问,但是不能放在这里(而应该通过允许匿名访问的方式来给请求放行)。「如果放在这里,登录请求将不走 SecurityContextPersistenceFilter
过滤器,也就意味着不会将登录用户信息存入 session,进而导致后续请求无法获取到登录用户信息。」