You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 18:44:56