迁移至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允许从请求参数获取令牌:
- 更新
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(); }
- 添加
BearerTokenResolverBean:
@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
相关产品推荐
相关产品推荐

