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

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授权码模式的标准实践,兼顾安全性和易用性:

  1. 后端在回调接口拿到Twitch的Token后,将Refresh Token存入HttpOnly、Secure、SameSite=Strict的Cookie中(防止XSS和CSRF);
  2. 重定向到React的登录成功页面时,不携带任何Token;
  3. React前端在成功页面加载后,立即向后端发起一个POST请求(比如/api/token/access),后端验证Cookie中的Refresh Token有效后,返回Access Token;
  4. 前端将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,可采用一次性凭证的方式:

  1. 后端在回调拿到Twitch Token后,生成一个短时效(比如5分钟)的一次性auth_code,将其与对应的Access Token、Refresh Token关联存储(比如Redis);
  2. 重定向到成功页面时,将auth_code作为查询参数传递(如/login/success?code=xxx);
  3. React前端拿到auth_code后,立即调用后端接口(比如/api/token/exchange),携带auth_code;
  4. 后端验证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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 15:26:06