Django中Authorization Code模式下如何向前端返回认证Token
问题分析
你的核心需求是在OAuth2授权码模式下,安全地将Access Token传递给React前端,同时保持Token认证模式(而非依赖Django会话)。你提到的两种方案确实存在安全隐患:
- Set-Cookie存Access Token:若未设置HttpOnly,易被XSS攻击窃取;
- URL哈希携带Token:虽不会被后端日志记录,但前端路由日志、第三方脚本仍可能获取,且不符合规范。
推荐解决方案
以下是符合安全规范的两种常用方案,结合你的Django+React场景适配:
方案1:HttpOnly Cookie存Refresh Token + 前端主动获取Access Token
这是OAuth2授权码模式的标准实践,兼顾安全性和易用性:
- 后端在回调接口拿到Twitch的Token后,将Refresh Token存入
HttpOnly、Secure、SameSite=Strict的Cookie中(防止XSS和CSRF); - 重定向到React的登录成功页面时,不携带任何Token;
- React前端在成功页面加载后,立即向后端发起一个
POST请求(比如/api/token/access),后端验证Cookie中的Refresh Token有效后,返回Access Token; - 前端将Access Token存储在内存(或sessionStorage,页面刷新后需重新获取),后续请求通过
Authorization: Bearer <token>头携带。
代码修改示例
Django视图修改:
import requests from django.conf import settings from django.contrib.auth import login, authenticate from django.http import JsonResponse, HttpResponseRedirect from rest_framework.views import APIView TWITCH_CLIENT_ID = settings.TWITCH_CLIENT_ID TWITCH_CLIENT_SECRET = settings.TWITCH_CLIENT_SECRET TWITCH_TOKEN_URL = settings.TWITCH_TOKEN_URL TWITCH_REDIRECT_URI = settings.TWITCH_REDIRECT_URI class TwitchLogin(APIView): def _generate_token(self, request): code = request.GET.get('code', None) if code is not None: token_params = { 'client_id': TWITCH_CLIENT_ID, 'client_secret': TWITCH_CLIENT_SECRET, 'code': code, 'redirect_uri': TWITCH_REDIRECT_URI, 'grant_type': 'authorization_code', } response = requests.post(TWITCH_TOKEN_URL, params=token_params) if response.ok: return response.json() return None def get(self, request, *args, **kwargs): if request.user.is_authenticated: return HttpResponseRedirect("/login/success") token_data = self._generate_token(request) if token_data: # 验证用户并登录(可选,若需关联Django用户) user = authenticate(request, token=token_data['access_token']) if user: login(request, user) # 设置Refresh Token的HttpOnly Cookie response = HttpResponseRedirect("/login/success") response.set_cookie( key='refresh_token', value=token_data['refresh_token'], httponly=True, secure=settings.DEBUG is False, # 生产环境开启 samesite='Strict', max_age=token_data['expires_in'] # 匹配Twitch的过期时间 ) return response return HttpResponseRedirect("/login/failed") # 新增获取Access Token的接口 class GetAccessToken(APIView): def post(self, request): refresh_token = request.COOKIES.get('refresh_token') if not refresh_token: return JsonResponse({'error': 'No refresh token'}, status=401) # 用Refresh Token向Twitch刷新Access Token(如果需要) refresh_params = { 'client_id': TWITCH_CLIENT_ID, 'client_secret': TWITCH_CLIENT_SECRET, 'refresh_token': refresh_token, 'grant_type': 'refresh_token', } response = requests.post(TWITCH_TOKEN_URL, params=refresh_params) if response.ok: token_data = response.json() # 更新Refresh Token Cookie(如果Twitch返回新的refresh_token) if 'refresh_token' in token_data: response = JsonResponse({'access_token': token_data['access_token']}) response.set_cookie( key='refresh_token', value=token_data['refresh_token'], httponly=True, secure=settings.DEBUG is False, samesite='Strict', max_age=token_data['expires_in'] ) return response return JsonResponse({'access_token': token_data['access_token']}) return JsonResponse({'error': 'Failed to refresh token'}, status=400)
React前端逻辑示例:
// login/success页面组件 import { useEffect, useState } from 'react'; import axios from 'axios'; function LoginSuccess() { const [accessToken, setAccessToken] = useState(null); useEffect(() => { // 页面加载后请求Access Token const fetchAccessToken = async () => { try { const response = await axios.post('/api/token/access'); setAccessToken(response.data.access_token); // 将token存入内存或sessionStorage sessionStorage.setItem('access_token', response.data.access_token); } catch (error) { console.error('获取Token失败:', error); // 跳转到登录失败页面 window.location.href = '/login/failed'; } }; fetchAccessToken(); }, []); if (!accessToken) { return <div>加载中...</div>; } return ( <div> <h1>登录成功</h1> {/* 后续业务逻辑 */} </div> ); } export default LoginSuccess;
方案2:一次性临时凭证换Token
如果不想依赖Cookie,可采用一次性凭证的方式:
- 后端在回调拿到Twitch Token后,生成一个短时效(比如5分钟)的一次性
auth_code,将其与对应的Access Token、Refresh Token关联存储(比如Redis); - 重定向到成功页面时,将
auth_code作为查询参数传递(如/login/success?code=xxx); - React前端拿到
auth_code后,立即调用后端接口(比如/api/token/exchange),携带auth_code; - 后端验证
auth_code有效后,返回Access Token和Refresh Token(Refresh Token仍建议存HttpOnly Cookie),同时销毁auth_code。
核心代码示例(Django部分)
import uuid import redis import json from django.conf import settings from django.http import JsonResponse, HttpResponseRedirect from rest_framework.views import APIView # 假设使用Redis存储临时凭证 redis_client = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0) class TwitchLogin(APIView): def _generate_token(self, request): # 复用之前的_generate_token逻辑 code = request.GET.get('code', None) if code is not None: token_params = { 'client_id': settings.TWITCH_CLIENT_ID, 'client_secret': settings.TWITCH_CLIENT_SECRET, 'code': code, 'redirect_uri': settings.TWITCH_REDIRECT_URI, 'grant_type': 'authorization_code', } response = requests.post(settings.TWITCH_TOKEN_URL, params=token_params) if response.ok: return response.json() return None def get(self, request, *args, **kwargs): if request.user.is_authenticated: return HttpResponseRedirect("/login/success") token_data = self._generate_token(request) if token_data: user = authenticate(request, token=token_data['access_token']) if user: login(request, user) # 生成一次性auth_code auth_code = str(uuid.uuid4()) # 存储到Redis,设置5分钟过期 redis_client.setex( f"auth_code:{auth_code}", 300, json.dumps({ 'access_token': token_data['access_token'], 'refresh_token': token_data['refresh_token'] }) ) # 重定向时携带auth_code return HttpResponseRedirect(f"/login/success?code={auth_code}") return HttpResponseRedirect("/login/failed") # 凭证交换接口 class ExchangeToken(APIView): def post(self, request): auth_code = request.data.get('code') if not auth_code: return JsonResponse({'error': 'Missing auth code'}, status=400) token_data_str = redis_client.get(f"auth_code:{auth_code}") if not token_data_str: return JsonResponse({'error': 'Invalid or expired auth code'}, status=400) # 删除一次性凭证 redis_client.delete(f"auth_code:{auth_code}") token_data = json.loads(token_data_str) # 设置Refresh Token Cookie response = JsonResponse({'access_token': token_data['access_token']}) response.set_cookie( key='refresh_token', value=token_data['refresh_token'], httponly=True, secure=settings.DEBUG is False, samesite='Strict' ) return response
方案对比
- 方案1更符合OAuth2安全规范,Refresh Token的安全性最高,适合大多数生产场景;
- 方案2不依赖Cookie(但Refresh Token仍建议存Cookie),适合对Cookie有特殊限制的场景,但需要额外的存储(如Redis)管理临时凭证。
内容的提问来源于stack exchange,提问作者MayonnaiseDolphin
相关产品推荐
相关产品推荐

