Google OAuth2令牌请求返回400 Bad Request问题排查求助
场景概述
我在Web应用中实现Google登录功能,使用Grails框架和Spring Security,授权流程完成后,回调阶段调用restTemplate.postForEntity请求令牌接口时返回400 Bad Request错误。已确认Google Console配置(权限范围、重定向URI、JavaScript源)正确,且等待配置生效、更换过重定向域名,问题仍存在。
控制器代码
import grails.plugin.springsecurity.SpringSecurityService import grails.plugin.springsecurity.annotation.Secured import groovy.json.JsonSlurper import org.springframework.web.client.RestTemplate import org.springframework.http.* @Secured('permitAll') class OAuthLoginController { SpringSecurityService springSecurityService // Step 1: Redirect to Google OAuth2 authorization endpoint def index() { def googleAuthUrl = "${grailsApplication.config.google.auth.url}?" + "client_id=${grailsApplication.config.google.client.id}&" + "redirect_uri=${grailsApplication.config.google.client.redirecturi}&" + "response_type=code&" + "scope=openid%20profile%20email" redirect url: googleAuthUrl } // Step 2: Handle Google callback and exchange code for access token def callback(String code) { if (!code) { flash.message = "Authorization failed." redirect uri: "/login/auth" return } def accessToken = exchangeCodeForAccessToken(code) if (!accessToken) { flash.message = "Failed to retrieve access token." redirect uri: "/login/auth" return } def userInfo = fetchUserInfo(accessToken) if (!userInfo?.email) { flash.message = "Failed to retrieve user info." redirect uri: "/login/auth" return } // Check if user exists or create new user def user = User.findByUsername(userInfo.email) ?: createUser(userInfo) springSecurityService.reauthenticate(user.username) redirect uri: "/home" } private String exchangeCodeForAccessToken(String code) { def restTemplate = new RestTemplate() def tokenRequestBody = [ code : code, client_id : grailsApplication.config.google.client.id, client_secret: grailsApplication.config.google.client.secret, redirect_uri : grailsApplication.config.google.client.redirecturi, grant_type : "authorization_code" ] def requestEntity = new HttpEntity<>(tokenRequestBody, new HttpHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED)) def response = restTemplate.postForEntity(grailsApplication.config.google.auth.tokenurl, requestEntity, String) def responseBody = new JsonSlurper().parseText(response.body) return responseBody.access_token } private Map fetchUserInfo(String accessToken) { def headers = new HttpHeaders() headers.set("Authorization", "Bearer $accessToken") def requestEntity = new HttpEntity<>(headers) def restTemplate = new RestTemplate() def response = restTemplate.exchange(grailsApplication.config.google.auth.userinfourl, HttpMethod.GET, requestEntity, String) return new JsonSlurper().parseText(response.body) } private User createUser(Map userInfo) { def user = new User(username: userInfo.email, password: "", enabled: true).save(flush: true) def role = Role.findByAuthority("ROLE_USER") ?: new Role(authority: "ROLE_USER").save(flush: true) UserRole.create(user, role, true) return user } }
配置信息
google: client: id: 'valid client id' secret: 'valid secret' redirecturi: 'https://localhost:8443/roadrace/oauth2/callback/google' auth: url: 'https://accounts.google.com/o/oauth2/v2/auth' tokenurl: 'https://oauth2.googleapis.com/token' userinfourl: 'https://www.googleapis.com/oauth2/v3/userinfo'
问题排查与解决方法
1. 请求体格式错误
当设置MediaType.APPLICATION_FORM_URLENCODED时,直接传入Map对象可能导致RestTemplate无法正确解析为URL编码格式,不符合Google令牌接口的要求。
解决方法:手动将Map转换为URL编码字符串:
import java.net.URLEncoder private String exchangeCodeForAccessToken(String code) { def restTemplate = new RestTemplate() def tokenParams = [ code : code, client_id : grailsApplication.config.google.client.id, client_secret: grailsApplication.config.google.client.secret, redirect_uri : grailsApplication.config.google.client.redirecturi, grant_type : "authorization_code" ] // 手动构建URL编码请求体 def requestBody = tokenParams.collect { k, v -> "${URLEncoder.encode(k, 'UTF-8')}=${URLEncoder.encode(v, 'UTF-8')}" }.join('&') def headers = new HttpHeaders() headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED) def requestEntity = new HttpEntity<>(requestBody, headers) def response = restTemplate.postForEntity(grailsApplication.config.google.auth.tokenurl, requestEntity, String) def responseBody = new JsonSlurper().parseText(response.body) return responseBody.access_token }
2. 重定向URI不匹配
Google要求回调阶段的redirect_uri参数必须与Google Console中配置的完全一致(包括协议、域名、端口、路径,大小写敏感)。
解决方法:
- 复制代码中
redirect_uri的完整值,粘贴到Google Console的重定向URI列表中 - 确保本地测试时使用的域名(如
localhost)与配置完全一致,localhost和127.0.0.1视为不同URI
3. 授权码重复使用
Google授权码(code)是一次性的,重复使用会触发400错误。
解决方法:每次测试重新触发授权流程,获取新的授权码,避免复用旧code。
4. 参数拼写或取值错误
检查grant_type是否严格为authorization_code,client_id和client_secret是否与Google Console中的值完全一致(注意引号、空格等特殊字符)。
解决方法:
- 调试输出
tokenRequestBody的所有参数,确认无拼写错误 - 使用Postman模拟令牌请求,传入相同参数验证是否能成功获取令牌,排除代码逻辑问题
5. HTTPS证书验证问题(本地测试)
本地环境使用自签名HTTPS证书时,RestTemplate可能无法验证证书,导致请求失败。
解决方法:开发环境临时配置RestTemplate忽略证书验证(生产环境禁用):
import org.apache.http.conn.ssl.SSLConnectionSocketFactory import org.apache.http.conn.ssl.TrustStrategy import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.impl.client.HttpClients import org.apache.http.ssl.SSLContexts import org.springframework.http.client.HttpComponentsClientHttpRequestFactory private RestTemplate createUnsafeRestTemplate() { TrustStrategy acceptingTrustStrategy = (chain, authType) -> true; SSLContext sslContext = SSLContexts.custom() .loadTrustMaterial(null, acceptingTrustStrategy) .build(); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); CloseableHttpClient httpClient = HttpClients.custom() .setSSLSocketFactory(csf) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); return new RestTemplate(requestFactory); }
在exchangeCodeForAccessToken和fetchUserInfo中使用此方法创建RestTemplate,并确保引入Apache HttpClient相关依赖。
内容的提问来源于stack exchange,提问作者kofhearts

