Django集成drf-social-oauth2调用convert-token报AccessDeniedError
环境与问题背景
- 运行环境:Python 3.9 + Django 4.0,项目已安装
drf-social-oauth2依赖 - 原有实现:基于已废弃的gapi OAuth2实现谷歌登录,功能运行正常,前端可获取
access_token传递给后端完成身份校验 - 切换原因:收到gapi服务即将停用的控制台警告,更换为新版Google Identity Service(GSI Client)
- 方案差异:新版GSI Client默认不再返回原有格式的
access_token,直接返回credential字段承载的JWT凭证,需要解码后才能获取用户详情 - 报错现象:将GSI返回的凭证提交到
/auth/convert-token/端点时,接口返回如下错误:
AccessDeniedError at /auth/convert-token (access_denied) Your credentials aren't allowed <oauthlib.Request SANITIZED>
已确认所有配置参数填写无误,已通过VSCode Thunder Client完成接口测试,问题排查受阻。
问题根因
drf-social-oauth2默认内置的谷歌OAuth2认证逻辑,仅支持接收谷歌返回的access_token,携带该token请求谷歌官方用户信息接口完成身份校验。新版GSI Client默认返回的credential是谷歌签发的ID Token(JWT格式),直接把这个值传给/auth/convert-token/端点时,后端会把它当成普通access_token请求谷歌用户信息接口,谷歌接口无法识别该类凭证,直接返回权限拒绝,最终抛出上述错误。
可行解决方案
二选一即可,优先推荐方案1,改动量最小。
方案1:前端调整GSI配置,返回标准access_token
GSI Client并非只能返回JWT格式的ID Token,可通过初始化配置调整为OAuth2授权模式,直接获取和旧版gapi格式一致的access_token,拿到后按原有逻辑传给/auth/convert-token/端点即可,无需修改后端代码。
配置要点:
- 初始化时不要使用一键登录默认返回
credential的配置,改用token_client方式初始化 - 授权scope配置包含
openid email profile - 最终取响应中的
access_token字段传递给后端,不要传递credential字段
方案2:自定义谷歌认证后端,兼容ID Token校验
如果不想调整前端逻辑,可自定义drf-social-oauth2的谷歌认证后端,增加JWT格式ID Token的校验逻辑,跳过无效的access_token请求步骤:
- 安装谷歌官方JWT校验依赖:
pip install google-auth - 在项目认证逻辑目录下新建
oauth_backends.py,写入自定义后端代码:
from google.oauth2 import id_token from google.auth.transport import requests from drf_social_oauth2.backends import GoogleOAuth2 from django.conf import settings class GoogleIdTokenOAuth2(GoogleOAuth2): def get_user_details(self, response): # 兼容原有access_token返回的用户数据格式 if 'sub' not in response: return super().get_user_details(response) # 适配ID Token解析出的用户字段 return { 'username': response.get('email'), 'email': response.get('email'), 'first_name': response.get('given_name', ''), 'last_name': response.get('family_name', ''), 'fullname': response.get('name', ''), } def get_user_id(self, details, response): if 'sub' in response: return response['sub'] return super().get_user_id(details, response) def request_user_data(self, token, *args, **kwargs): # 识别JWT格式的ID Token if len(token.split('.')) == 3: try: # 校验JWT签名、过期时间、受众 id_info = id_token.verify_oauth2_token( token, requests.Request(), settings.GOOGLE_OAUTH2_CLIENT_ID ) # 校验token签发方为谷歌 if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: raise ValueError('Invalid token issuer') return id_info except Exception: pass # 非ID Token走原有access_token认证逻辑 return super().request_user_data(token, *args, **kwargs)
- 修改项目
settings.py中的认证后端配置,替换默认的谷歌认证后端:
AUTHENTICATION_BACKENDS = ( # 替换为自定义的谷歌认证后端,注意替换为实际的项目路径 'your_project.xxx.oauth_backends.GoogleIdTokenOAuth2', # 保留原有其他认证后端 'django.contrib.auth.backends.ModelBackend', )
- 前端传参时,
token字段直接传GSI返回的credential值即可,其余参数保持原有规则不变。
排查注意事项
- 确认谷歌云控制台的OAuth2客户端配置,已将当前前端域名加入「已授权的JavaScript来源」,后端回调地址加入「已授权的重定向URI」
- 安全提示:使用方案2时必须通过谷歌官方库校验JWT的签名、受众、签发方、过期时间,禁止直接无校验解码JWT提取用户信息,否则会存在任意用户身份伪造的高危安全漏洞
- 接口请求时
grant_type字段固定为convert_token,client_id、client_secret需和django-oauth-toolkit中创建的应用参数一致,backend字段固定为google
内容的提问来源于stack exchange,提问作者Ernest Alidza
相关产品推荐
相关产品推荐

