SpringSecurity如何放行资源

SpringSecurity配置放行资源

permitAll配置实例

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/css/**", "/js/**","/fonts/**").permitAll()
                .anyRequest().authenticated();
    }
}

web ignore配置实例 

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public 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,用户登录成功之后,有两种方式获取用户登录信息:

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

这两种办法,都可以获取到当前登录用户信息。

这两种方式获取到的数据都是来自 SecurityContextHolderSecurityContextHolder 中的数据,本质上是保存在 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 过滤器链。

 

小结

总之,前端静态资源放行时,可以直接不走 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 {

//配置不需要认证的资源路径
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                
                .authorizeHttpRequests(conf -> conf.requestMatchers(SystemConstants.HTTP_ACT_MATCHERS).permitAll()
                        .anyRequest()..authenticated()).build();
    }

    /**
     * 配置全局的某些通用事物,例如静态资源等
     * @return
     */
    @Bean
    public WebSecurityCustomizer securityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/static/**");
    }

}

如果项目是前后端分离项目,通过token交互的,我们一般会定义token过滤器,但是会出现SpringSecurity 配置permitAll之后仍然会走自定义过滤器Filter的问题,看到网上解决方案是将token过滤器filter不要注册为bean而是交给security管理并且配置WebSecurityCustomizer securityCustomizer(),代码如大体如下

@Configuration
@EnableWebSecurity
public class MySecurityConfig {
    @Bean
    public 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
     */
    @Bean
    public WebSecurityCustomizer securityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/static/**");
    }
}

但是本地实测,放行的info和login还是会走token 过滤器,其实走token过滤器没啥影响,我们只需在token过滤器判断跳过主要token校验逻辑即可,token过滤器参考代码如下

@Component
public class JwtFilter extends OncePerRequestFilter {
    @Override
    protected 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();
        }
    }
}

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序三两行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值