事情的原由是这样,有小伙伴在微信上问了松哥一个问题:css
就是他使用 Spring Security 作用户登陆,等成功后,结果没法获取到登陆用户信息,松哥以前写过相关的文章(奇怪,Spring Security 登陆成功后老是获取不到登陆用户信息?),可是他彷佛没有看懂。考虑到这是一个很是常见的问题,所以我想今天换个角度再来和大伙聊一聊这个话题。html
Spring Security 中,到底该怎么样给资源额外放行?前端
在 Spring Security 中,有一个资源,若是你但愿用户不用登陆就能访问,那么通常来讲,你有两种配置策略:java
第一种就是在 configure(WebSecurity web) 方法中配置放行,像下面这样:web
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode"); }
第二种方式是在 configure(HttpSecurity http) 方法中进行配置:spring
http.authorizeRequests() .antMatchers("/hello").permitAll() .anyRequest().authenticated()
两种方式最大的区别在于,第一种方式是不走 Spring Security 过滤器链,而第二种方式走 Spring Security 过滤器链,在过滤器链中,给请求放行。后端
在咱们使用 Spring Security 的时候,有的资源可使用第一种方式额外放行,不须要验证,例如前端页面的静态资源,就能够按照第一种方式配置放行。微信
有的资源放行,则必须使用第二种方式,例如登陆接口。你们知道,登陆接口也是必需要暴露出来的,不须要登陆就能访问到的,可是咱们却不能将登陆接口用第一种方式暴露出来,登陆请求必需要走 Spring Security 过滤器链,由于在这个过程当中,还有其余事情要作。session
接下来我以登陆接口为例,来和小伙伴们分析一下走 Spring Security 过滤器链有什么不一样。ide
首先你们知道,当咱们使用 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()
好了,这就是和小伙伴们分享的两种资源放行策略,你们千万别搞错了哦~
有收获的话,记得点个在看鼓励下松哥哦~