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

Google OAuth2令牌请求返回400 Bad Request问题排查求助

问题: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 06:54:56