Spring Boot3+Security6 OAuth2授权码模式无法兑换令牌问题
使用Spring Boot 3.3.3 + Spring Security 6.4.2对接第三方OAuth2服务(非Google/Github等),目标是获取用户授权以拿到开发者数据,流程如下:
- 用户被重定向至第三方登录页;
- 用户完成登录;
- 用户收到授权请求;
- 我方成功获取第三方的授权码;
- 需要用授权码兑换access token和refresh token(后续无需用户保持登录)。
当前问题:Spring在第4步后无法自动向令牌端点兑换令牌,但用Postman可以成功兑换。日志显示授权码已到达/oauth2/authorization/cool-client,但未触发令牌兑换,调试Controller也未被命中。
相关配置与代码
application.yml配置
spring: application: name: glorious-name security: oauth2: client: registration: cool-client: provider: server client-id: cool-client-s-app-id client-secret: cool-client-s-app-secret authorization-grant-type: authorization_code redirect-uri: http://localhost:8080/oauth2/authorization/cool-client client-authentication-method: client_credentials scope: scopes_that_matter state: true provider: server: authorization-uri: https://server.com/oauth/oauth2/auth token-uri: https://server.com/oauth/oauth2/token
执行日志
417+02:00 DEBUG 232660 --- [glorious-name] [nio-8581-exec-1] [hash1] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/oauth2/authorization/cool-client 424+02:00 DEBUG 232660 --- [glorious-name] [nio-8581-exec-2] [hash2] o.s.security.web.FilterChainProxy : Securing GET /oauth2/authorization/cool-client 431+02:00 DEBUG 232660 --- [glorious-name] [nio-8581-exec-2] [hash2] o.s.s.web.DefaultRedirectStrategy : Redirecting to https://server.com/oauth/oauth2/auth?response_type=code&client_id=cool-client-id&scope=encoded_scopes&state=...D&redirect_uri=http://localhost:8080/oauth2/authorization/cool-client 540+02:00 DEBUG 232660 --- [glorious-name] [nio-8581-exec-3] [hash3] o.s.security.web.FilterChainProxy : Securing GET /oauth2/authorization/cool-client?code=...&scope=encoded_scopes&state=... 544+02:00 DEBUG 232660 --- [glorious-name] [nio-8581-exec-3] [hash3] o.s.s.web.DefaultRedirectStrategy : Redirecting to https://server.com/oauth/oauth2/auth?response_type=code&client_id=cool_client_id&scope=encoded_scopes&state=...&redirect_uri=http://localhost:8080/oauth2/authorization/cool-client
调试Controller(尝试过多种写法)
@GetMapping("/oauth2/authorization/cool-client") // public ResponseEntity<String> exchangeCodeForToken(@RequestParam(name = "code") String code, @RequestParam(name = "state") String state, @RequestParam(name = "scope") String scope) { // public ResponseEntity<String> exchangeCodeForToken(@RegisteredOAuth2AuthorizedClient("cool-client") OAuth2AuthorizedClient authorizedClient) { public ResponseEntity<String> exchangeCodeForToken(@AuthenticationPrincipal OAuth2User user) { // log.info("!!!!! SUCCESS !!!!!" + authorizedClient.getClientRegistration().getRegistrationId()); log.info("USER " + user.getAttributes()); // log.info("!!!!! THE CODE!!!!! from authorization/lh" + code); return ResponseEntity.ok("Successfully reading the auth code"); }
SecurityConfig.java
@Slf4j @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/", "/error", "/login/**", "/oauth2/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth -> oauth .failureUrl("/oauth2/error") ) .oauth2Client(Customizer.withDefaults()); return http.build(); }
用户疑问:需要修改哪些配置/代码实现正确流程?是否必须自定义令牌兑换逻辑?还有哪些调试技巧?
核心配置修正
1. 修正redirect-uri配置
/oauth2/authorization/cool-client是Spring Security用于发起授权请求的端点,不是接收授权码的回调地址。回调地址应该用Spring OAuth2 Login默认的回调端点{baseUrl}/login/oauth2/code/{registrationId},同时要在第三方OAuth2服务后台配置完全一致的回调地址。
修改application.yml中的redirect-uri:
redirect-uri: "{baseUrl}/login/oauth2/code/cool-client"
2. 修正client-authentication-method
授权码模式下,客户端认证方式应该用client_secret_basic或client_secret_post,client_credentials是客户端凭证模式的认证方式,不适合当前场景。根据第三方服务要求调整,比如:
client-authentication-method: client_secret_post
3. 补充provider用户信息配置(可选)
如果需要获取用户信息,补充第三方的用户信息端点配置:
provider: server: authorization-uri: https://server.com/oauth/oauth2/auth token-uri: https://server.com/oauth/oauth2/token user-info-uri: https://server.com/oauth/oauth2/userinfo user-name-attribute: id # 根据第三方返回的用户唯一标识字段调整
代码调整
1. 监听登录成功事件获取令牌
Spring Security会自动完成授权码兑换令牌的流程,无需手动编写兑换逻辑。可以通过自定义登录成功处理器来获取令牌:
@Bean public SecurityFilterChain filterChain(HttpSecurity http, OAuth2AuthorizedClientService authorizedClientService) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/", "/error", "/login/**", "/oauth2/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth -> oauth .failureUrl("/oauth2/error") .successHandler((request, response, authentication) -> { // 从认证信息中提取授权客户端 OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication; OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient( authToken.getAuthorizedClientRegistrationId(), authToken.getName() ); // 获取accessToken和refreshToken String accessToken = client.getAccessToken().getTokenValue(); String refreshToken = client.getRefreshToken() != null ? client.getRefreshToken().getTokenValue() : null; log.info("Access Token: {}", accessToken); log.info("Refresh Token: {}", refreshToken); // 重定向到业务页面 response.sendRedirect("/success"); }) ) .oauth2Client(Customizer.withDefaults()); return http.build(); }
2. 移除无效的调试Controller
原Controller映射的是授权请求端点,不会被授权码回调触发,直接删除或修改映射路径为自定义业务端点即可。
是否需要自定义令牌兑换逻辑?
不需要。只要配置符合OAuth2标准规范,Spring Security OAuth2 Login会自动完成授权码兑换令牌的流程。只有当第三方服务的OAuth2实现不符合RFC规范时,才需要自定义OAuth2AccessTokenResponseClient来适配。
额外调试技巧
- 开启OAuth2相关TRACE日志,重点关注
OAuth2AuthorizationCodeAuthenticationProvider和DefaultAuthorizationCodeTokenResponseClient的日志,查看令牌请求的参数、响应及错误信息:
logging: level: org.springframework.security.oauth2: TRACE
- 抓包对比Postman和Spring发出的令牌请求,检查请求头、参数(如
grant_type、code、redirect_uri)是否完全一致; - 验证第三方返回的state参数是否和Spring发送的一致,Spring会验证state防止CSRF,不一致会触发重定向回授权页(这就是日志中最后一步重定向的原因);
- 确认第三方服务后台配置的回调地址和Spring配置的
redirect-uri完全一致,包括协议(http/https)、端口、路径。
内容的提问来源于stack exchange,提问作者Mateva

