Spring Security 两种资源放行策略,千万别用错了!

事情的原由是这样,有小伙伴在微信上问了松哥一个问题:css

就是他使用 Spring Security 作用户登陆,等成功后,结果没法获取到登陆用户信息,松哥以前写过相关的文章(奇怪,Spring Security 登陆成功后老是获取不到登陆用户信息?),可是他彷佛没有看懂。考虑到这是一个很是常见的问题,所以我想今天换个角度再来和大伙聊一聊这个话题。html

Spring Security 中,到底该怎么样给资源额外放行?前端

1.两种思路

在 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

2.登陆请求分析

首先你们知道,当咱们使用 Spring Security,用户登陆成功以后,有两种方式获取用户登陆信息:

  1. SecurityContextHolder.getContext().getAuthentication()
  2. 在 Controller 的方法中,加入 Authentication 参数

这两种办法,均可以获取到当前登陆用户信息。具体的操做办法,你们能够看看松哥以前发布的教程: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());
        }
    }
}

本来的方法很长,我这里列出来了比较关键的几个部分:

  1. SecurityContextPersistenceFilter 继承自 GenericFilterBean,而 GenericFilterBean 则是 Filter 的实现,因此 SecurityContextPersistenceFilter 做为一个过滤器,它里边最重要的方法就是 doFilter 了。
  2. 在 doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository,读取 SecurityContext 的操做会进入到 readSecurityContextFromSession 方法中,在这里咱们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是 SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。
  3. SecurityContext 是一个接口,它有一个惟一的实现类 SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。
  4. 在拿到 SecurityContext 以后,经过 SecurityContextHolder.setContext 方法将这个 SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security 的后续操做,咱们均可以直接从 SecurityContextHolder 中获取到用户信息了。
  5. 接下来,经过 chain.doFilter 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter 过滤器中了)。
  6. 在过滤器链走完以后,数据响应给前端以后,finally 中还有一步收尾操做,这一步很关键。这里从 SecurityContextHolder 中获取到 SecurityContext,获取到以后,会把 SecurityContextHolder 清空,而后调用 repo.saveContext 方法将获取到的 SecurityContext 存入 session 中。

至此,整个流程就很明了了。

每个请求到达服务端的时候,首先从 session 中找出来 SecurityContext ,而后设置到 SecurityContextHolder 中去,方便后续使用,当这个请求离开的时候,SecurityContextHolder 会被清空,SecurityContext 会被放回 session 中,方便下一个请求来的时候获取。

登陆请求来的时候,尚未登陆用户数据,可是登陆请求走的时候,会将用户登陆数据存入 session 中,下个请求到来的时候,就能够直接取出来用了。

看了上面的分析,咱们能够至少得出两点结论:

  1. 若是咱们暴露登陆接口的时候,使用了前面提到的第一种方式,没有走 Spring Security,过滤器链,则在登陆成功后,就不会将登陆用户信息存入 session 中,进而致使后来的请求都没法获取到登陆用户信息(后来的请求在系统眼里也都是未认证的请求)
  2. 若是你的登陆请求正常,走了 Spring Security 过滤器链,可是后来的 A 请求没走过滤器链(采用前面提到的第一种方式放行),那么 A 请求中,也是没法经过 SecurityContextHolder 获取到登陆用户信息的,由于它一开始没通过 SecurityContextPersistenceFilter 过滤器链。

3.小结

总之,前端静态资源放行时,能够直接不走 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()

好了,这就是和小伙伴们分享的两种资源放行策略,你们千万别搞错了哦~

有收获的话,记得点个在看鼓励下松哥哦~

相关文章
相关标签/搜索