为何OncePerRequestFilter在permitAll()端点仍执行?Spring Security疑问
在Spring Security配置中使用SecurityFilterChain,通过authorizeHttpRequests设置/user/register等端点无需认证(permitAll()),但通过addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)添加的自定义JWT过滤器仍会在这些公开端点执行,因无令牌导致抛出JWTDecodeException。
已通过在OncePerRequestFilter中重写shouldNotFilter()方法跳过这些路径解决问题,但存在疑问:原以为permitAll()会绕过所有认证相关处理,想明确:
SecurityFilterChain中OncePerRequestFilter与authorizeHttpRequests的执行顺序addFilterBefore是否会无视authorizeHttpRequests规则始终执行过滤器- 跳过公开端点过滤器的标准模式
相关配置代码:
@Bean @Order(2) public SecurityFilterChain userFilterChain( HttpSecurity http, @Qualifier("userAuthManager") AuthenticationManager userAuthManager) throws Exception { return http.securityMatcher("/user/**") .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) .authorizeHttpRequests( auth -> auth.requestMatchers("/user/me", "/user/update/**", "/user/logout") .authenticated() .anyRequest() .permitAll()) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationManager(userAuthManager) .addFilterBefore(userJWTFilter, UsernamePasswordAuthenticationFilter.class) .build(); }
@RestController @RequestMapping("/user") public class AuthController { ... @PostMapping("/register") public ResponseEntity<?> registerUser( ... } ... }
1. 执行顺序:过滤器链先于授权规则
Spring Security的请求处理流程是过滤器链优先执行,授权决策(authorizeHttpRequests)后置处理:
- 所有通过
addFilterBefore/addFilterAfter添加的过滤器,会在请求到达授权逻辑前依次运行 authorizeHttpRequests由过滤器链中的AuthorizationFilter负责处理,属于链条的后期环节
简单说,permitAll()只是授权阶段的放行规则,不会提前跳过前面的过滤器执行。你的JWT过滤器在UsernamePasswordAuthenticationFilter之前,而AuthorizationFilter在更靠后的位置,所以JWT过滤器必然先运行,和后续授权规则无关。
2. addFilterBefore确实会无视授权规则执行
addFilterBefore/addFilterAfter的作用是将过滤器插入到指定位置的过滤器链中,只要请求匹配当前SecurityFilterChain的securityMatcher(这里是/user/**),过滤器就会执行,完全不受authorizeHttpRequests规则影响。
permitAll()不负责跳过过滤器,它仅在授权阶段允许请求通过,不会改变过滤器链的执行流程。
3. 跳过公开端点过滤器的标准模式
模式一:重写shouldNotFilter()方法(当前使用方案)
在自定义OncePerRequestFilter中重写该方法,直接指定需要跳过的路径:
@Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getRequestURI(); return path.equals("/user/register") || path.equals("/user/login"); }
此方式灵活度高,适合自定义复杂路径规则。
模式二:过滤器内部判断路径直接放行
若不想重写shouldNotFilter(),可在doFilterInternal()开头判断路径,直接放行:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String path = request.getRequestURI(); if (path.startsWith("/user/register") || path.startsWith("/user/login")) { filterChain.doFilter(request, response); return; } // 原JWT校验逻辑 }
模式三:拆分SecurityFilterChain(推荐设计)
创建两个独立的过滤器链,分别处理公开端点和需认证端点:
// 公开端点过滤器链,优先级更高(Order(1)) @Bean @Order(1) public SecurityFilterChain publicUserFilterChain(HttpSecurity http) throws Exception { return http.securityMatcher("/user/register", "/user/login") .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) // 不添加JWT过滤器 .build(); } // 需认证端点过滤器链,优先级较低(Order(2)) @Bean @Order(2) public SecurityFilterChain securedUserFilterChain( HttpSecurity http, @Qualifier("userAuthManager") AuthenticationManager userAuthManager) throws Exception { return http.securityMatcher("/user/me", "/user/update/**", "/user/logout") .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationManager(userAuthManager) .addFilterBefore(userJWTFilter, UsernamePasswordAuthenticationFilter.class) .build(); }
这种方式符合Spring Security的设计思想,让公开端点完全不经过JWT过滤器,无需额外路径判断。
内容的提问来源于stack exchange,提问作者Hi Byee

