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 了。
在 doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是
HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到 readSecurityContextFromSession 方法中,在这里我们看到了读取的核心方法Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。SecurityContext是一个接口,它有一个唯一的实现类SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。在拿到 SecurityContext 之后,通过
SecurityContextHolder.setContext方法将这个 SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从 SecurityContextHolder 中获取到用户信息了。接下来,通过 chain.doFilter 让请求继续向下走(这个时候就会进入到
UsernamePasswordAuthenticationFilter过滤器中了)。在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从
SecurityContextHolder中获取到 SecurityContext,获取到之后,会把SecurityContextHolder清空,然后调用repo.saveContext方法将获取到的 SecurityContext 存入 session 中。 至此,整个流程就很明了了。每一个请求到达服务端的时候,首先从 session 中找出来
SecurityContext,然后设置到SecurityContextHolder中去,方便后续使用,当这个请求离开的时候,SecurityContextHolder会被清空,SecurityContext会被放回 session 中,方便下一个请求来的时候获取。登录请求来的时候,还没有登录用户数据,但是登录请求走的时候,会将用户登录数据存入 session 中,下个请求到来的时候,就可以直接取出来用了。
- 如果我们暴露登录接口的时候,使用了前面提到的第一种方式,没有走 Spring Security,过滤器链,则在登录成功后,就不会将登录用户信息存入 session 中,进而导致后来的请求都无法获取到登录用户信息(后来的请求在系统眼里也都是未认证的请求)
- 如果你的登录请求正常,走了 Spring Security 过滤器链,但是后来的 A 请求没走过滤器链(采用前面提到的第一种方式放行),那么 A 请求中,也是无法通过
SecurityContextHolder获取到登录用户信息的,因为它一开始没经过SecurityContextPersistenceFilter过滤器链。
