Spring Boot Security结合Firebase认证:白名单URL失效问题排查
问题排查:Spring Boot API白名单路径仍触发认证过滤器
问题描述
用Java Spring Boot搭建带基础CRUD的API,希望/api/v1/habit/**路径加入白名单无需认证,/user等其他路径需认证保护。但当前所有请求(包括白名单路径)都会触发Firebase认证过滤器,提示必须携带认证Token。
相关代码
AuthenticationFilter
public class FirebaseAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final String AUTH_HEADER = "Authorization"; public FirebaseAuthenticationFilter() { super(request -> true); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { final String token = request.getHeader(AUTH_HEADER); if(token == null) { throw new IllegalArgumentException("No Auth Token in header - " + AUTH_HEADER); } try { FirebaseToken firebaseToken = FirebaseAuth.getInstance().verifyIdToken(token); AbstractAuthenticationToken authenticationToken = new FirebaseAuthenticationToken(firebaseToken); return getAuthenticationManager().authenticate(authenticationToken); } catch (FirebaseException e) { throw new IllegalArgumentException(e.getMessage()); } } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // This updates the SecurityContextHolder super.successfulAuthentication(request, response, chain, authResult); chain.doFilter(request,response); } }
Authentication Provider
@Component public class FirebaseAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { FirebaseAuthenticationToken firebaseAuthenticationToken = (FirebaseAuthenticationToken) authentication; firebaseAuthenticationToken.setAuthenticated(true); return firebaseAuthenticationToken; } @Override public boolean supports(Class<?> authentication) { return FirebaseAuthenticationToken.class.isAssignableFrom(authentication); } }
AuthenticationToken(Model)
public class FirebaseAuthenticationToken extends AbstractAuthenticationToken { private final FirebaseToken firebaseToken; public FirebaseAuthenticationToken(FirebaseToken firebaseToken) { super(null); this.firebaseToken = firebaseToken; setAuthenticated(true); } @Override public Object getCredentials() { return firebaseToken; } public String getEmail() { return firebaseToken.getEmail(); } public String getAuthUid() { return firebaseToken.getUid(); } @Override public Object getPrincipal() { return firebaseToken.getClaims(); } @Override public boolean implies(Subject subject) { return super.implies(subject); } }
SecurityConfig
@RequiredArgsConstructor @Configuration @EnableWebSecurity public class SecurityConfig { final private FirebaseAuthenticationProvider firebaseAuthenticationProvider; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { FirebaseAuthenticationFilter filter = new FirebaseAuthenticationFilter(); //AuthenticationManager is responsible for invoking the authentication provider(s) to authenticate the user. filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); http .csrf().disable() .authenticationProvider(firebaseAuthenticationProvider) .authorizeHttpRequests((authz) -> authz .requestMatchers("/api/v1/habit/**").permitAll() .anyRequest().authenticated() ) .addFilterAt(filter, BasicAuthenticationFilter.class); return http.build(); } }
问题原因
核心问题出在FirebaseAuthenticationFilter的构造函数:
public FirebaseAuthenticationFilter() { super(request -> true); }
这里传入的request -> true表示所有请求都会触发这个过滤器,完全无视SecurityConfig里配置的白名单规则。因为Spring Security的过滤器链中,认证过滤器的执行优先级高于授权规则判断,所以即使是permitAll的路径,也会先进入这个过滤器的attemptAuthentication方法,而该方法强制要求请求携带Authorization头,导致白名单路径也被拦截。
解决方案
方案1:修改过滤器的请求匹配规则(推荐)
修改FirebaseAuthenticationFilter的构造函数,只对需要认证的路径生效,或者排除白名单路径:
方式A:指定需要认证的路径
比如只拦截/api/v1/user/**等需要认证的路径:
public FirebaseAuthenticationFilter() { super(new AntPathRequestMatcher("/api/v1/user/**")); }
如果有多个需要认证的路径,可以用OrRequestMatcher组合:
public FirebaseAuthenticationFilter() { super(new OrRequestMatcher( new AntPathRequestMatcher("/api/v1/user/**"), new AntPathRequestMatcher("/api/v1/other-protected/**") )); }
方式B:排除白名单路径
如果需要保护的路径太多,更高效的方式是排除白名单路径:
public FirebaseAuthenticationFilter() { super(request -> !new AntPathRequestMatcher("/api/v1/habit/**").matches(request)); }
方案2:在过滤器内判断路径并放行
在attemptAuthentication方法开头,先检查请求路径是否属于白名单,如果是则直接放行(不过这种方式不如方案1规范,因为过滤器应该只处理需要认证的请求):
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws AuthenticationException, IOException, ServletException { // 检查是否是白名单路径 if(new AntPathRequestMatcher("/api/v1/habit/**").matches(request)){ chain.doFilter(request, response); return null; } // 原有认证逻辑 final String token = request.getHeader(AUTH_HEADER); if(token == null) { throw new IllegalArgumentException("No Auth Token in header - " + AUTH_HEADER); } // ... 剩余代码 }
额外优化点
FirebaseAuthenticationToken构造函数里直接调用setAuthenticated(true)会绕过认证提供者的逻辑,建议去掉这行,让FirebaseAuthenticationProvider来处理认证状态。attemptAuthentication中抛出的IllegalArgumentException可以替换为Spring Security提供的BadCredentialsException或AuthenticationServiceException,更符合Spring Security的异常体系。
内容的提问来源于stack exchange,提问作者Minon Weerasinghe
相关产品推荐
相关产品推荐

