今日干货
来看今天的正文。
事情的起因是这样,有小伙伴在微信上问了松哥一个问题:
就是他使用 Spring Security 做用户登录,等成功后,结果无法获取到登录用户信息,松哥之前写过相关的文章(奇怪,Spring Security 登录成功后总是获取不到登录用户信息?),但是他似乎没有看懂。考虑到这是一个非常常见的问题,因此我想今天换个角度再来和大伙聊一聊这个话题。
Spring Security 中,到底该怎么样给资源额外放行?
在 Spring Security 中,有一个资源,如果你希望用户不用登录就能访问,那么一般来说,你有两种配置策略:
第一种就是在 configure(WebSecurity web) 方法中配置放行,像下面这样:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
}
第二种方式是在 configure(HttpSecurity http) 方法中进行配置:
http.authorizeRequests()
.antMatchers("/hello").permitAll()
.anyRequest().authenticated()
两种方式最大的区别在于,第一种方式是不走 Spring Security 过滤器链,而第二种方式走 Spring Security 过滤器链,在过滤器链中,给请求放行。
在我们使用 Spring Security 的时候,有的资源可以使用第一种方式额外放行,不需要验证,例如前端页面的静态资源,就可以按照第一种方式配置放行。
有的资源放行,则必须使用第二种方式,例如登录接口。大家知道,登录接口也是必须要暴露出来的,不需要登录就能访问到的,但是我们却不能将登录接口用第一种方式暴露出来,登录请求必须要走 Spring Security 过滤器链,因为在这个过程中,还有其他事情要做。
接下来我以登录接口为例,来和小伙伴们分析一下走 Spring Security 过滤器链有什么不同。
首先大家知道,当我们使用 Spring Security,用户登录成功之后,有两种方式获取用户登录信息:
SecurityContextHolder.getContext().getAuthentication()
这两种办法,都可以获取到当前登录用户信息。具体的操作办法,大家可以看看松哥之前发布的教程:Spring Security 如何动态更新已登录用户信息?。
这两种方式获取到的数据都是来自 SecurityContextHolder,SecurityContextHolder 中的数据,本质上是保存在 ThreadLocal
中,ThreadLocal
的特点是存在它里边的数据,哪个线程存的,哪个线程才能访问到。
这样就带来一个问题,当用户登录成功之后,将用户用户数据存在 SecurityContextHolder 中(thread1),当下一个请求来的时候(thread2),想从 SecurityContextHolder 中获取用户登录信息,却发现获取不到!为啥?因为它俩不是同一个 Thread。
但实际上,正常情况下,我们使用 Spring Security 登录成功后,以后每次都能够获取到登录用户信息,这又是怎么回事呢?
这我们就要引入 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 中,方便下一个请求来的时候获取。
登录请求来的时候,还没有登录用户数据,但是登录请求走的时候,会将用户登录数据存入 session 中,下个请求到来的时候,就可以直接取出来用了。
看了上面的分析,我们可以至少得出两点结论:
总之,前端静态资源放行时,可以直接不走 Spring Security 过滤器链,像下面这样:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico");
}
后端的接口要额外放行,就需要仔细考虑场景了,不过一般来说,不建议使用上面这种方式,建议下面这种方式,原因前面已经说过了:
http.authorizeRequests()
.antMatchers("/hello").permitAll()
.anyRequest().authenticated()
好了,这就是和小伙伴们分享的两种资源放行策略,大家千万别搞错了哦~