如何在Django中存储Google OAuth2令牌?解决refresh_token缺失问题
解决Google OAuth2无法获取Refresh Token的问题
问题出在前端使用的gapi默认采用隐式授权流程(Implicit Flow),该流程本身不返回refresh_token;同时即使切换到授权码流程,也需要配置特定参数才能触发Google返回refresh_token。下面是具体的解决方案:
1. 切换到授权码流程(Authorization Code Flow)
这是获取refresh_token的核心前提,隐式流程不支持长期令牌刷新。需要前端手动构造授权URL,而非依赖gapi的默认逻辑:
前端(Vue.js)授权跳转代码
// 构造Google授权URL const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${YOUR_GOOGLE_CLIENT_ID}&redirect_uri=${encodeURIComponent(window.location.origin + '/auth/google/callback')}&response_type=code&scope=${encodeURIComponent('openid email profile https://www.googleapis.com/auth/classroom.courses.readonly')}&access_type=offline&prompt=consent`; window.location.href = authUrl;
关键参数说明:
response_type=code:指定使用授权码流程access_type=offline:强制Google返回refresh_token(必须参数)prompt=consent:确保每次授权都弹出确认框,避免因用户已授权过而不返回refresh_token(首次授权后可根据需求调整)scope:必须包含Classroom所需权限(示例为只读课程权限,根据实际需求添加)
前端回调处理
在回调页面中提取授权码,发送给后端换取令牌:
// 回调页面的mounted钩子 const code = new URLSearchParams(window.location.search).get('code'); if (code) { axios.post('/auth/google/exchange-token', { code }) .then(res => { // 保存后端返回的内部令牌 localStorage.setItem('internal_token', res.data.access_token); window.location.href = '/'; }); }
2. 后端处理令牌交换与存储
后端收到授权码后,调用Google的令牌接口换取完整的令牌信息(包含refresh_token),并存储到你的GoogleOAuth2Credentials模型中:
后端自定义令牌交换视图
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status import requests from django.conf import settings from .models import GoogleOAuth2Credentials from django.contrib.auth.models import User from social_django.utils import load_strategy, load_backend from oauth2_provider.models import AccessToken class GoogleExchangeTokenView(APIView): def post(self, request): code = request.data.get('code') if not code: return Response({'error': '授权码不能为空'}, status=status.HTTP_400_BAD_REQUEST) # 调用Google令牌接口换取access_token、refresh_token token_payload = { 'code': code, 'client_id': settings.GOOGLE_CLIENT_ID, 'client_secret': settings.GOOGLE_CLIENT_SECRET, 'redirect_uri': settings.GOOGLE_REDIRECT_URI, 'grant_type': 'authorization_code' } token_res = requests.post('https://oauth2.googleapis.com/token', data=token_payload) token_data = token_res.json() if 'error' in token_data: return Response(token_data, status=status.HTTP_400_BAD_REQUEST) # 获取用户信息,关联本地用户 userinfo = requests.get( 'https://openidconnect.googleapis.com/v1/userinfo', headers={'Authorization': f'Bearer {token_data["access_token"]}'} ).json() user, _ = User.objects.get_or_create(email=userinfo['email'], defaults={'username': userinfo['email'].split('@')[0]}) # 存储Google令牌(支持更新已存在的记录) GoogleOAuth2Credentials.objects.update_or_create( user=user, defaults={ 'access_token': token_data['access_token'], 'refresh_token': token_data.get('refresh_token'), # 仅首次授权或重新授权时存在 'id_token': token_data.get('id_token'), 'scope': token_data['scope'].split(' ') } ) # 生成后端内部令牌(复用drf-social-oauth2逻辑) strategy = load_strategy(request) backend = load_backend(strategy, 'google-oauth2', redirect_uri=settings.GOOGLE_REDIRECT_URI) backend.do_auth(token_data['access_token']) # 返回内部令牌给前端 internal_token = AccessToken.objects.get(user=user, application__name='你的应用名称') return Response({ 'access_token': internal_token.token, 'refresh_token': internal_token.refresh_token.token if internal_token.refresh_token else None })
3. 关键配置与注意事项
- Google控制台配置:确保
redirect_uri已添加到OAuth客户端的授权回调地址列表中,同时启用Google Classroom API - Refresh Token有效期:Google的refresh_token长期有效,但如果用户撤销权限或超过令牌限制(每个用户每个客户端最多50个),会失效,需要重新触发授权
- 令牌加密:继续使用
EncryptedTextField存储敏感令牌,避免明文泄露
内容的提问来源于stack exchange,提问作者Samuele B.
相关产品推荐
相关产品推荐

