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

Django集成Google登录遇invalid_grant错误求助(Flutter前端)

Django + Flutter Google登录集成:invalid_grant 错误排查

问题背景

我基于Django开发后端、Flutter开发前端,在集成Google登录授权时遇到问题:Flutter端获取auth_code并发送给Django后,后端调用Google令牌接口返回错误:{'error': 'invalid_grant', 'error_description': 'Bad Request'}。即使改用URL参数传递请求参数(如下方curl示例),Postman测试仍返回相同错误。

后端核心代码

GoogleAuth服务类

google_auth_request = HttpRequestManager()

class GoogleAuth:
    def __init__(self, code):
        self.code = code

    def get_access_token(self, code: str, redirect_uri: str) -> str:
        data = {
            "code": code,
            "client_id": config.settings.constants.GOOGLE_AUTH_CONFIG["GOOGLE_OAUTH2_CLIENT_ID"],
            "client_secret": config.settings.constants.GOOGLE_AUTH_CONFIG["GOOGLE_OAUTH2_CLIENT_SECRET"],
            "redirect_uri": redirect_uri,
            "grant_type": "authorization_code",
        }

        response = google_auth_request.post(
            config.settings.constants.GOOGLE_AUTH_CONFIG["GOOGLE_ACCESS_TOKEN_OBTAIN_URL"], data=data
        )
        if not response.ok:
            log.error("Response.json() for get access_token")
            log.error(response.json())
            print(response.json(), flush=True)
            raise ValidationError("Could not get access token from Google.")

        access_token = response.json()["access_token"]

        return access_token

    def get_user_info(self, access_token: str) -> Dict[str, Any]:
        response = google_auth_request.get(
            config.settings.constants.GOOGLE_AUTH_CONFIG["GOOGLE_USER_INFO_URL"], params={"access_token": access_token}
        )

        if not response.ok:
            log.error(f"google auth response data: {response.data}")
            print(response.json(), flush=True)
            raise Exception("google service is unavailable")

        return response.json()

    def login(self):
        domain = config.settings.constants.GOOGLE_AUTH_CONFIG["GOOGLE_OAUTH2_API_URL"]
        redirect_uri = f"{domain}/api/auth/v1/login/google/"

        access_token = self.get_access_token(code=self.code, redirect_uri=redirect_uri)
        user_data = self.get_user_info(access_token)
        # 首次登录时创建用户
        query = EtloUser.objects.filter(email=user_data["email"])
        if not query.exists():
            EtloUser.objects.create(
                email=user_data["email"],
                first_name=user_data.get("given_name"),
                last_name=user_data.get("family_name"),
            )

        profile_data = {
            "email": user_data["email"],
            "first_name": user_data.get("given_name"),
            "last_name": user_data.get("family_name"),
        }
        return profile_data

视图代码

@extend_schema(request=GoogleAuthSerializer, responses={200: OutputCredentialSerializer})
class GoogleAuthView(APIView):
    throttle_scope = "auth"

    def post(self, request, *args, **kwargs):
        auth_serializer = GoogleAuthSerializer(data=request.data)
        auth_serializer.is_valid(raise_exception=True)
        validated_data = auth_serializer.validated_data

        if validated_data["error"] or not validated_data["code"]:
            params = urlencode({"error": validated_data["error"]})
            return redirect(f"{config.settings.constants.GOOGLE_AUTH_CONFIG['GOOGLE_OAUTH2_LOGIN_URL']}?{params}")
        google_auth_service = GoogleAuth(validated_data["code"])
        user_profile = google_auth_service.login()

        auth_backend = EtloGoogleAuthenticationBackend(user_profile["email"])
        user = auth_backend.authenticate_credentials()
        tokens = auth_backend.get_user_tokens_data(user)
        return Response(tokens, status=status.HTTP_200_OK)

测试用curl请求

curl --location --request POST 'https://oauth2.googleapis.com/token?client_id=******-c4hrkmu0sfc1t3se6ni5g44aacg81iia.apps.googleusercontent.com&client_secret=******-0lkcEi489FxaLzUwRGyF5MmAgE3T&code=******InQyhphLF2Cjjx75fCiiTCAVRtiZ3fNrUdwNXpwTSH0_-Vbrzl4VrKSQ&redirect_uri=https%3A%2F%2Fetlo-firebase.firebaseapp.com%2F__%2Fauth%2Fhandler&grant_type=authorization_code'

返回错误

{
    "error": "invalid_grant",
    "error_description": "Bad Request"
}

排查要点

  • redirect_uri 完全匹配:
    确保Django中使用的redirect_uri({domain}/api/auth/v1/login/google/)与Flutter请求Google授权时的redirect_uri完全一致,包括大小写、结尾斜杠、协议(http/https)。Google OAuth对该参数的匹配要求严格,任何细微差异都会触发invalid_grant。
  • auth_code 有效性检查:
    auth_code是一次性凭证,只能使用一次,重复调用会直接失效;同时code有效期仅10分钟,超时也会报错。检查Flutter是否重复发送同一code,或后端是否复用了已使用过的code。
  • 客户端凭证正确性:
    确认Django配置的GOOGLE_OAUTH2_CLIENT_ID和GOOGLE_OAUTH2_CLIENT_SECRET与Google Cloud Console中创建的OAuth 2.0客户端凭证完全一致,注意区分Web/Android/iOS类型的客户端ID,避免混用。
  • 请求内容类型验证:
    Google令牌接口要求请求体为application/x-www-form-urlencoded格式,检查HttpRequestManager的post方法是否正确设置了Content-Type头。若默认使用application/json,会导致参数解析失败,触发错误。
  • Google Cloud配置检查:
    登录Google Cloud Console,确认对应OAuth客户端ID的「授权重定向URI」列表中,已添加Django使用的redirect_uri和测试用的https://etlo-firebase.firebaseapp.com/__/auth/handler,未添加的URI会被拒绝。
  • IP/地区限制排查:
    检查Google Cloud中该OAuth客户端是否设置了IP或地区限制,若后端服务器IP不在允许列表内,会导致请求被拦截。

内容的提问来源于stack exchange,提问作者Emad Helmi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 17:57:09