• anyRequest | 匹配所有请求路径

  • access | SpringEl表达式结果为true时可以访问

  • anonymous | 匿名可以访问

  • denyAll | 用户不能访问

  • fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)

  • hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问

  • hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问

  • hasAuthority | 如果有参数,参数表示权限,则其权限可以访问

  • hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问

  • hasRole | 如果有参数,参数表示角色,则其角色可以访问

  • permitAll | 用户可以任意访问

  • rememberMe | 允许通过remember-me登录的用户访问

  • authenticated | 用户登录后可访问

  • 第一种就是在 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,用户登录成功之后,有两种方式获取用户登录信息:

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

  • 这两种方式获取到的数据都是来自 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());
  }
 }
}
  • SecurityContextPersistenceFilter 继承自 GenericFilterBean,而 GenericFilterBean 则是 Filter 的实现,所以 SecurityContextPersistenceFilter 作为一个过滤器,它里边最重要的方法就是 doFilter 了。
  1. 在 doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到 readSecurityContextFromSession 方法中,在这里我们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是 SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。

  2. SecurityContext 是一个接口,它有一个唯一的实现类 SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。

  3. 在拿到 SecurityContext 之后,通过 SecurityContextHolder.setContext 方法将这个 SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从 SecurityContextHolder 中获取到用户信息了。

  4. 接下来,通过 chain.doFilter 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter 过滤器中了)。

  5. 在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从 SecurityContextHolder 中获取到 SecurityContext,获取到之后,会把 SecurityContextHolder 清空,然后调用 repo.saveContext 方法将获取到的 SecurityContext 存入 session 中。 至此,整个流程就很明了了。

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

  7. 登录请求来的时候,还没有登录用户数据,但是登录请求走的时候,会将用户登录数据存入 session 中,下个请求到来的时候,就可以直接取出来用了。

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