SpringSecurity配置放行资源
permitAll配置实例
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/css/**", "/js/**","/fonts/**").permitAll().anyRequest().authenticated();}
}
web ignore配置实例
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/css/**");web.ignoring().antMatchers("/js/**");web.ignoring().antMatchers("/fonts/**");}
}
两种方式最大的区别在于,第一种方式是不走 Spring Security 过滤器链,而第二种方式走 Spring Security 过滤器链,在过滤器链中,给请求放行。
在我们使用 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
过滤器链。
小结
总之,前端静态资源放行时,可以直接不走 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()
如果使用的是security6版本,以上两种方式稍有不同,如下
@Configuration
@EnableWebSecurity
public class MySecurityConfig {//配置不需要认证的资源路径@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {return http.authorizeHttpRequests(conf -> conf.requestMatchers(SystemConstants.HTTP_ACT_MATCHERS).permitAll().anyRequest()..authenticated()).build();}/*** 配置全局的某些通用事物,例如静态资源等* @return*/@Beanpublic WebSecurityCustomizer securityCustomizer() {return (web) -> web.ignoring().requestMatchers("/static/**");}}
如果项目是前后端分离项目,通过token交互的,我们一般会定义token过滤器,但是会出现SpringSecurity 配置permitAll之后仍然会走自定义过滤器Filter的问题,看到网上解决方案是将token过滤器filter不要注册为bean而是交给security管理并且配置WebSecurityCustomizer securityCustomizer(),代码如大体如下
@Configuration
@EnableWebSecurity
public class MySecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {//不需要认证的请求return http.authorizeHttpRequests(conf -> conf.requestMatchers("/login", "/info").permitAll().anyRequest().authenticated())//通过new JwtFilter()的方式,而不是bean组件注入的方式.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class).csrf(withDefaults()).cors(withDefaults()).build();}/*** 配置全局的某些通用事物,例如静态资源等* @return*/@Beanpublic WebSecurityCustomizer securityCustomizer() {return (web) -> web.ignoring().requestMatchers("/static/**");}
}
但是本地实测,放行的info和login还是会走token 过滤器,其实走token过滤器没啥影响,我们只需在token过滤器判断跳过主要token校验逻辑即可,token过滤器参考代码如下
@Component
public class JwtFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");//放行if (token == null || request.getRequestURI().equals("info")){filterChain.doFilter(request, response);return;}try {String username = JwtUtils.getValue(token, "username");UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(User.withUsername(username), null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}catch (Exception e){e.printStackTrace();}}
}