Spring Security多SecurityFilterChain配置问题及permitAll安全性疑问
问题背景
我在Spring Security中配置了两类SecurityFilterChain:
- 一类为
/api路径提供OAuth2验证 - 另一类为
/net路径通过X-Authorization头中的密钥完成授权
核心配置代码
@Configuration @EnableWebSecurity public class SecurityConfiguration { private final XAuthorizationFilter xAuthorizationFilter; public SecurityConfiguration(XAuthorizationFilter xAuthorizationFilter) { this.xAuthorizationFilter= xAuthorizationFilter; } @Bean @Order(1) public SecurityFilterChain xAuthorizationSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher( "/net/**" ) .addFilterAt(this.xAuthorizationFilter, AuthorizationFilter.class) .authorizeHttpRequests(auth -> auth // .requestMatchers("/net/url1").hasRole(NET) .requestMatchers("/net/url2").hasRole(NET) ); return http.build(); } @Bean @Order(2) public SecurityFilterChain oAuth2SecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher( "/api/**", "/health" ) .authorizeHttpRequests(auth -> auth // .requestMatchers("/api/admin/**").hasRole(ADMIN) .requestMatchers("/api/**").hasRole(EMPLOYEE) .requestMatchers("/health").permitAll() // .anyRequest().denyAll() // ) .oauth2ResourceServer(oauth2 -> oauth2 // .jwt(jwt -> jwt // .jwtAuthenticationConverter(new KeycloakJwtConverter()) // ) // ) // .logout(logout -> logout // .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) // ); return http.build(); } }
自定义X-Authorization验证过滤器代码
@Component public class XAuthorizationFilter implements Filter { @Value("${net.authorization}") private String netAuthorization; @Value("${net.endpoints}") private String[] netEndpoints; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (this.isNetEndpoint(request)) { String header = request.getHeader("X-Authorization"); if (header == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Missing X-Authorization header."); return; } if (!this.netAuthorization.equals(header)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("Invalid X-Authorization header."); return; } SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_NET"); Authentication authentication = new UsernamePasswordAuthenticationToken("xUser", null, Collections.singleton(authority)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private boolean isNetEndpoint(HttpServletRequest request) { return Arrays.stream(this.netEndpoints).anyMatch(endpoint -> endpoint.equals(request.getRequestURI())); } @Override public void init(FilterConfig filterConfig) { // no implementation needed } @Override public void destroy() { // no implementation needed } }
遇到的问题
调用/net/url1等接口时,返回401错误(提示缺少Bearer令牌),说明请求触发了OAuth2过滤器链。调整配置后,在第二个SecurityFilterChain中添加.anyRequest().permitAll()才能正常运行,想咨询这个permitAll配置是否存在安全风险?
解答
问题根源:过滤器链的匹配与授权逻辑不统一
你遇到的/net/url1触发OAuth2链的问题,本质是第一个过滤器链的匹配范围和自定义过滤器的处理范围不一致:
- 第一个链用
securityMatcher("/net/**")匹配所有/net开头的路径,但自定义过滤器只对net.endpoints配置的精确URI(比如/net/url1)做认证处理; - 当请求进入第一个链后,如果过滤器没设置认证信息(比如URI不匹配精确配置),第一个链的授权规则要求
hasRole(NET),此时会因无认证信息触发拒绝,但Spring Security的链匹配逻辑如果没处理好,会让请求意外落到第二个OAuth2链,导致返回Bearer令牌缺失的401。
.anyRequest().permitAll()的安全风险
直接在第二个链加这个配置有明确安全隐患:
- 第二个链的
securityMatcher覆盖/api/**和/health,.anyRequest().permitAll()会让所有未被之前规则覆盖的/api路径(比如拼写错误的/api/xxx、新增未配置的接口)都允许匿名访问,绕过OAuth2验证; - 后续新增
/api下的接口时,若忘记更新授权规则,这些接口会默认开放,极易引发未授权访问漏洞。
正确的修复方案
方案1:统一/net路径的匹配与过滤逻辑
把自定义过滤器的端点判断改成前缀匹配,覆盖所有/net/**路径:
private boolean isNetEndpoint(HttpServletRequest request) { return request.getRequestURI().startsWith("/net/"); }
同时简化第一个链的授权规则:
.authorizeHttpRequests(auth -> auth .anyRequest().hasRole(NET) )
这样所有/net开头的请求都会经过过滤器验证X-Authorization头,通过后设置ROLE_NET权限,完全由第一个链处理,不会流向第二个链。
方案2:精确控制第一个链的授权范围
如果必须只开放指定/net端点,调整第一个链的授权规则,明确拒绝其他/net路径:
.authorizeHttpRequests(auth -> auth .requestMatchers("/net/url1", "/net/url2").hasRole(NET) .requestMatchers("/net/**").denyAll() )
这样不在白名单的/net路径会被第一个链直接拒绝,不会触发第二个链。
方案3:缩小第一个链的匹配范围
如果只有/net/url1、/net/url2需要X-Authorization验证,修改第一个链的securityMatcher为精确匹配:
.securityMatcher("/net/url1", "/net/url2")
这样只有这两个路径走第一个链,其他/net路径会被Spring Security默认拒绝(返回403),避免流向第二个链。
结论
绝对不要用.anyRequest().permitAll()来临时解决问题,这会给/api路径带来未授权访问的风险。通过调整第一个链的匹配、过滤和授权逻辑,就能让/net路径的请求完全由对应链处理,从根源解决问题。
内容的提问来源于stack exchange,提问作者Claudio Zesiger

