Spring Boot接口无认证时返回403而非401的问题解决
问题:缺少JWT Token时返回403而非401,白名单URL访问异常
问题详情
API在请求缺少认证Token时,当前返回403 Forbidden,但预期应返回401 Unauthorized。通过JwtAuthenticationFilter处理JWT认证,该过滤器在Token缺失或无效时会允许请求无认证继续执行。需要修改代码实现:
- Token缺失时返回401而非403
- 指定白名单URL无需认证,不会返回401
无认证请求受保护端点的日志
2023-06-13T14:32:44.524+02:00 DEBUG 5652 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy : Securing GET /api/v1/user 2023-06-13T14:32:44.525+02:00 DEBUG 5652 --- [nio-8080-exec-3] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext 2023-06-13T14:32:44.535+02:00 DEBUG 5652 --- [nio-8080-exec-3] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access 2023-06-13T14:32:44.537+02:00 DEBUG 5652 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy : Securing GET /error 2023-06-13T14:32:44.540+02:00 DEBUG 5652 --- [nio-8080-exec-3] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext 2023-06-13T14:32:44.541+02:00 DEBUG 5652 --- [nio-8080-exec-3] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
现有代码
JwtAuthenticationFilter
package com.user.secretcommander.security; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { final String authorizationHeader = request.getHeader(SecurityConstants.getAuthorizationHeader()); final String jwt; final String username; if (authorizationHeader == null || !authorizationHeader.startsWith(SecurityConstants.getBearerPrefix())) { filterChain.doFilter(request, response); return; } jwt = authorizationHeader.substring(SecurityConstants.getBearerPrefix().length()); username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } filterChain.doFilter(request, response); } }
SecurityConfig
package com.user.secretcommander.config; import com.user.secretcommander.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private static final String[] AUTH_WHITELIST = { // General "/", // Auth "/api/v1/auth/**", // Swagger UI "/api-docs", "/api-docs/**", "/swagger-ui.html", "/swagger-ui/**" }; private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests() .requestMatchers(AUTH_WHITELIST).permitAll() .anyRequest().authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }
尝试过的修改及问题
修改JwtAuthenticationFilter中的Token缺失判断逻辑:
if (authorizationHeader == null || !authorizationHeader.startsWith(SecurityConstants.getBearerPrefix())) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; }
问题:白名单URL(如Swagger UI)无法访问,因为所有无Token的请求都被强制返回401,包括白名单内的请求。
解决方案
1. 提取白名单到公共常量类
创建SecurityConstants类,统一管理认证相关常量和白名单,方便过滤器访问:
public class SecurityConstants { public static final String AUTHORIZATION_HEADER = "Authorization"; public static final String BEARER_PREFIX = "Bearer "; public static final String[] AUTH_WHITELIST = { "/", "/api/v1/auth/**", "/api-docs", "/api-docs/**", "/swagger-ui.html", "/swagger-ui/**" }; }
同时修改SecurityConfig,移除内部的AUTH_WHITELIST,改用SecurityConstants.AUTH_WHITELIST。
2. 修改JwtAuthenticationFilter,先放行白名单请求
在过滤器中先判断请求路径是否属于白名单,是则直接放行;否则再处理Token缺失逻辑:
@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { final String requestPath = request.getServletPath(); // 检查是否为白名单URL,是则直接放行 for (String whiteListUrl : SecurityConstants.AUTH_WHITELIST) { if (pathMatcher.match(whiteListUrl, requestPath)) { filterChain.doFilter(request, response); return; } } final String authorizationHeader = request.getHeader(SecurityConstants.AUTHORIZATION_HEADER); final String jwt; final String username; if (authorizationHeader == null || !authorizationHeader.startsWith(SecurityConstants.BEARER_PREFIX)) { // 非白名单URL且无Token,返回401 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Missing or invalid authentication token"); return; } jwt = authorizationHeader.substring(SecurityConstants.BEARER_PREFIX.length()); username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } filterChain.doFilter(request, response); } }
3. 配置SecurityConfig,替换默认的403 EntryPoint
默认的Http403ForbiddenEntryPoint会返回403,需要配置自定义的AuthenticationEntryPoint返回401:
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests() .requestMatchers(SecurityConstants.AUTH_WHITELIST).permitAll() .anyRequest().authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) // 自定义认证异常处理,返回401 .exceptionHandling() .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Unauthorized access"); }); return http.build(); } }
方案说明
- 白名单判断优先:确保白名单URL不受Token校验逻辑影响,正常放行
- 非白名单URL无Token时直接返回401:提前拦截无效请求
- 替换Security异常处理入口:解决匿名用户访问受保护资源时返回403的问题,统一返回401
内容的提问来源于stack exchange,提问作者Sven Märki
相关产品推荐
相关产品推荐

