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

迁移至Spring Security 6后Spring应用出现403 Forbidden错误

Spring Boot 2.5迁移至3.1后OAuth2授权头问题排查

我们正从Spring Boot 2.5迁移至3.1版本,调整安全配置后遇到授权相关问题。修改代码后出现新错误:日志显示未找到Bearer Token,同时抛出org.springframework.web.bind.MissingRequestHeaderException异常,提示缺少必需的Authorization请求头。

安全配置代码

package com.xxx.xx.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

/**
 * The Class CustomWebSecurityConfigurerAdapter.
 */
@Configuration
@EnableWebSecurity
@EnableCaching
public class CustomWebSecurityConfigurerAdapter {

    @Configuration
    @Order(1)
    public static class ActuatorWebSecurityConfig {
        @Bean
        public SecurityFilterChain filterChainBasic(HttpSecurity http) throws Exception {
            return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth ->
                    auth.requestMatchers("/actuator/**").permitAll()
                            .requestMatchers(EndpointRequest.to("info", "health")).permitAll() // To bypass security for this Endpoint's
                            .anyRequest().hasRole("ACTUATOR")
            ).build();
        }
    }

    @Configuration
    @EnableMethodSecurity
    @Order(2)
    public static class WebSecurityConfiguration {


        @Value("${auth0.audience}") private String audience;


        @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") private String issuer;


        /** The AccessDeniedHandler handles all request that are denied. */
        @Autowired
        private OAuth2AccessDeniedHandler oAuth2AccessDeniedHandler;

        /** The OAuth2AuthenticationEntryPoint handles all request that are authenticated via OAuth2. */
        @Autowired
        private OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint;



        /*
         * (non-Javadoc)
         *
         * @see org.springframework.security.config.annotation.web.configuration. WebSecurityConfigurerAdapter#configure(org.springframework.security.config. annotation.web.builders.HttpSecurity)
         */

        @Bean
        public SecurityFilterChain filterChainOAuth2(final HttpSecurity http) throws Exception {

            System.setProperty("https.proxyHost", "XXX.XXX.XXX.com");
            System.setProperty("https.proxyPort", "XXXX");
            System.setProperty("https.proxySet","true");
            return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth ->
                            auth.anyRequest().authenticated()
                    )
                    .oauth2ResourceServer((oauth2ResourceServer) ->
                            oauth2ResourceServer.authenticationEntryPoint(oAuth2AuthenticationEntryPoint)
                                    .accessDeniedHandler(oAuth2AccessDeniedHandler)
                                    .jwt((jwt) ->
                                            jwt.decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter())
                                    )
                    ).sessionManagement((sessionManagement) ->
                            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    )
                    .build();
        }

        protected JwtAuthenticationConverter jwtAuthenticationConverter() {
            MyJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MyJwtGrantedAuthoritiesConverter();
            JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
            jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
            return jwtAuthenticationConverter;
        }

        @Bean
        protected JwtDecoder jwtDecoder() {
            NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                    JwtDecoders.fromOidcIssuerLocation(issuer);

            OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
            OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
            OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

            jwtDecoder.setJwtValidator(withAudience);

            return jwtDecoder;
        }



        @Bean
        public WebSecurityCustomizer webSecurityCustomizer() {
            // @formatter:off
            // security will not apply to these end points, they will be totally open
            return (web) -> web.ignoring().requestMatchers(
                    "/configuration/ui",
                    "/swagger-resources/**",
                    "/configuration/**",
                    "/swagger-ui/**",
                    "/v3/api-docs/**",
                    "/v3/api-docs/",
                    "/swagger-ui.html",
                    "/swagger.json",
                    "/webjars/**",
                    "/favicon.ico",
                    "/api/swagger-ui/**");
            // @formatter:on

        }

    }
}

错误日志

[2023-07-20T14:40:52.421Z] [TRACE] Invoking BearerTokenAuthenticationFilter (7/13)
[2023-07-20T14:40:52.421Z] [TRACE] Did not process request since did not find bearer token
[2023-07-20T14:40:52.421Z] [TRACE] Invoking RequestCacheAwareFilter (8/13)
[2023-07-20T14:40:52.421Z] [TRACE] Invoking SecurityContextHolderAwareRequestFilter (9/13)
[2023-07-20T14:40:52.421Z] [TRACE] Invoking AnonymousAuthenticationFilter (10/13)
[2023-07-20T14:40:52.421Z] [TRACE] Invoking SessionManagementFilter (11/13)
[2023-07-20T14:40:52.421Z] [TRACE] Created SecurityContextImpl [Null authentication]
[2023-07-20T14:40:52.421Z] [TRACE] Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
[2023-07-20T14:40:52.421Z] [TRACE] Invoking ExceptionTranslationFilter (12/13)
[2023-07-20T14:40:52.421Z] [TRACE] Invoking AuthorizationFilter (13/13)
[2023-07-20T14:40:52.422Z] [TRACE] Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@711e5ae6]
[2023-07-20T14:40:52.422Z] [TRACE] Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@711e5ae6] using org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer$$Lambda$1635/0x000000080178d548@4ab5ceba
[2023-07-20T14:40:52.422Z] [DEBUG] Secured POST /api/v1/manage-op-profiles/associations/?access_token=<OAUTH2_ACCESS_GENERATED_TOKEN>
[2023-07-20T14:40:52.423Z] [TRACE] Not injecting HSTS header since it did not match request to [Is Secure]
[2023-07-20T14:40:52.423Z] [WARN ] Resolved [org.springframework.web.bind.MissingRequestHeaderException: Required request header 'Authorization' for method parameter type String is not present

问题分析

从日志可见,请求通过access_tokenURL参数传递令牌,但Spring Security 5.8+(对应Spring Boot 3.x)的BearerTokenAuthenticationFilter默认仅从Authorization请求头解析Bearer Token,不再支持从URL参数读取。这导致过滤器无法识别令牌,后续接口因强制要求Authorization头而抛出异常。

解决方案

方案1:启用URL参数解析令牌(不推荐生产环境)

修改OAuth2资源服务器配置,添加自定义BearerTokenResolver允许从请求参数获取令牌:

  1. 更新filterChainOAuth2方法:
@Bean
public SecurityFilterChain filterChainOAuth2(final HttpSecurity http) throws Exception {

    System.setProperty("https.proxyHost", "XXX.XXX.XXX.com");
    System.setProperty("https.proxyPort", "XXXX");
    System.setProperty("https.proxySet","true");
    return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth ->
                    auth.anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2ResourceServer) ->
                    oauth2ResourceServer.authenticationEntryPoint(oAuth2AuthenticationEntryPoint)
                            .accessDeniedHandler(oAuth2AccessDeniedHandler)
                            .bearerTokenResolver(bearerTokenResolver()) // 添加此行
                            .jwt((jwt) ->
                                    jwt.decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter())
                            )
            ).sessionManagement((sessionManagement) ->
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .build();
}
  1. 添加BearerTokenResolver Bean:
@Bean
public BearerTokenResolver bearerTokenResolver() {
    DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
    resolver.setAllowQueryToken(true); // 允许从URL参数获取token
    return resolver;
}

注意:URL参数易被日志记录,存在令牌泄露风险,生产环境建议改用Authorization: Bearer <token>请求头传递令牌。

方案2:调整接口代码

若接口方法使用@RequestHeader("Authorization")强制读取请求头,可改为通过SecurityContextHolder获取认证信息,无需直接读取请求头:

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;

// 在接口方法中获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();
String tokenValue = jwt.getTokenValue(); // 如需令牌内容

内容的提问来源于stack exchange,提问作者Rishy Rane

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 15:09:51