Spring Security 5 & OAuth2配置:令牌校验请求缺失token参数致400错误
问题描述
我的应用接收第三方应用携带Bearer Token的Authorization头请求,需要通过授权服务器校验令牌。但当前发送的校验请求未携带服务器要求的token参数,导致400 Bad Request错误。
- 当前校验请求:
GET https://myApp.com/connect/tokenValidation - 期望校验请求:
GET https://myApp.com/connect/tokenValidation?token=eySomething
以下是我的配置代码及相关日志:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class ResourceServerConfiguration { @Bean public SecurityFilterChain oAuth2filterChain(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**", "/rest/**") .and() .authorizeRequests() .anyRequest() .authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .oauth2ResourceServer() .jwt(); return http.build(); } @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder .withJwkSetUri("https://myApp.com/connect/tokenValidation") .jwtProcessorCustomizer(jwtProcessor -> jwtProcessor.setJWSTypeVerifier( new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt")))) .build(); return jwtDecoder; } }
相关日志:
org.springframework.security.authentication.AuthenticationServiceException: An error occurred while attempting to decode the Jwt: Couldn't retrieve remote JWK set: org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: ""token is missing"" at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:104) ~[spring-security-oauth2-resource-server-5.7.2.jar:5.7.2] at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.authenticate(JwtAuthenticationProvider.java:88) ~[spring-security-oauth2-resource-server-5.7.2.jar:5.7.2] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-5.7.5.jar:5.7.5] at org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:134) ~[spring-security-oauth2-resource-server-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183) ~[spring-security-web-5.7.2.jar:5.7.2] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.64] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.64] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[catalina.jar:9.0.64] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.64] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[catalina.jar:9.0.64] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[catalina.jar:9.0.64] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.64] at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687) ~[catalina.jar:9.0.64] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.64] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[catalina.jar:9.0.64] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-coyote.jar:9.0.64] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.64] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[tomcat-coyote.jar:9.0.64] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1787) ~[tomcat-coyote.jar:9.0.64] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.64] at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-util.jar:9.0.64] at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-util.jar:9.0.64] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.64] at java.lang.Thread.run(Thread.java:829) ~[?:?] Caused by: org.springframework.security.oauth2.jwt.JwtException: An error occurred while attempting to decode the Jwt: Couldn't retrieve remote JWK set: org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: ""token is missing"" at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.createJwt(NimbusJwtDecoder.java:169) ~[spring-security-oauth2-jose-5.7.2.jar:5.7.2] at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.decode(NimbusJwtDecoder.java:137) ~[spring-security-oauth2-jose-5.7.2.jar:5.7.2] at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:97) ~[spring-security-oauth2-resource-server-5.7.2.jar:5.7.2] ... 45 more
解决方案
你当前的核心问题是误用了withJwkSetUri——这个方法是用来从指定地址获取JWKS密钥集(用于本地验证JWT签名)的,默认不会携带任何token参数。而你的授权服务器要求必须通过URL参数token来校验令牌,所以需要完全自定义JwtDecoder的实现,手动调用授权服务器的校验接口并传递token参数。
修改后的配置如下:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class ResourceServerConfiguration { private final RestTemplate restTemplate = new RestTemplate(); private static final String VALIDATION_URL = "https://myApp.com/connect/tokenValidation?token={token}"; @Bean public SecurityFilterChain oAuth2filterChain(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**", "/rest/**") .and() .authorizeRequests() .anyRequest() .authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .oauth2ResourceServer() .jwt() .decoder(jwtDecoder()); return http.build(); } @Bean public JwtDecoder jwtDecoder() { return token -> { // 调用授权服务器校验token,自动填充token参数到URL ResponseEntity<String> response = restTemplate.getForEntity(VALIDATION_URL, String.class, token); if (!response.getStatusCode().is2xxSuccessful()) { throw new JwtException("Token validation failed with status: " + response.getStatusCode()); } // 根据授权服务器的实际返回结构调整Jwt对象构建逻辑 // 示例1:如果返回完整JWT字符串,可直接解析 // return NimbusJwtDecoder.withJwtProcessor(new DefaultJWTProcessor<>()).build().decode(response.getBody()); // 示例2:如果返回自定义用户信息,手动构建Jwt return Jwt.withTokenValue(token) .claims(claims -> { // 这里替换为从响应体提取实际claims的逻辑 // 比如用ObjectMapper解析响应为Map后放入claims }) .build(); }; } }
关键说明
VALIDATION_URL中的{token}是RestTemplate的占位符,调用时会自动替换为待校验的token值,生成符合要求的请求格式。- 需根据授权服务器的返回结构调整Jwt对象的构建逻辑:
- 若验证通过后返回完整JWT字符串,可复用
NimbusJwtDecoder解析该字符串。 - 若返回自定义的用户信息、权限等,需手动解析响应体并填充到Jwt的claims中。
- 若验证通过后返回完整JWT字符串,可复用
- 可扩展异常处理逻辑,针对401、403等状态码抛出对应认证异常,让Spring Security正确处理认证失败场景。
内容的提问来源于stack exchange,提问作者Mr P
相关产品推荐
相关产品推荐

