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

如何在Django中以HttpOnly Cookie方式传递JWT Refresh Token

问题描述

我用Django(REST Framework、SimpleJWT)和React开发项目,采用JWT身份验证。根据资料,把Refresh Token存储在HttpOnly Cookie中是最安全的方案,但作为Web开发学习者,我没找到完整实现方法。我尝试重写了MyTokenObtainPairView的post方法,但还需要实现登录和刷新Token时都将Refresh Token以HttpOnly Cookie方式发送给前端。以下是我目前的代码:


现有代码

views.py

class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer
    def post(self, request, *args, **kwargs):
        try:
            response = super().post(request)
        except InvalidToken:
            return Response({'Invalid or expired token'})
        refresh_token = RefreshToken.for_user(request.user)
        response.set_cookie('refresh_token', refresh_token, httponly=True)
        return response

class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    permission_classes = (AllowAny,)
    serializer_class = RegisterSerializer

class LogoutView(generics.CreateAPIView):
    def post(self, request):
        refresh_token = request.data['refresh_token']
        token = RefreshToken(refresh_token)
        token.blacklist()
        return Response({'Logout':'Successfullly'})

urls.py

path('api/login/', MyTokenObtainPairView.as_view(), name="token_obtain_pair"),
    path('api/token/refresh/', TokenRefreshView.as_view(), name="token_refresh"),
    path('api/register/', RegisterView.as_view(), name="auth_register"),
    path('api/logout/', LogoutView.as_view(), name="logout"),

settings.py

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(hours=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=2),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',

    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(hours=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=2),
}

完整实现方案

1. 修复登录视图(MyTokenObtainPairView)

原代码重新生成Refresh Token属于冗余操作,直接从响应数据中获取SimpleJWT生成的refresh token即可,同时补充Cookie的安全属性(生产环境建议开启secure):

from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.response import Response
from rest_framework import status

class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        # 从响应数据中获取refresh token
        refresh_token = response.data.get('refresh')
        if refresh_token:
            # 设置HttpOnly Cookie
            response.set_cookie(
                key='refresh_token',
                value=refresh_token,
                httponly=True,
                secure=True,  # 生产环境开启,仅HTTPS传输
                samesite='Lax',  # 防止CSRF,根据场景选Strict/Lax
                max_age=2 * 24 * 3600  # 和Refresh Token有效期一致(2天)
            )
            # 可选:从响应中移除refresh字段,不让前端拿到
            del response.data['refresh']
        return response

2. 重写TokenRefreshView实现刷新时更新Cookie

默认的TokenRefreshView不会更新Cookie,需要重写它,在刷新后把新的refresh token写入Cookie:

from rest_framework_simplejwt.views import TokenRefreshView
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError

class MyTokenRefreshView(TokenRefreshView):
    def post(self, request, *args, **kwargs):
        # 从Cookie中获取refresh token,而非请求体
        request.data['refresh'] = request.COOKIES.get('refresh_token')
        try:
            response = super().post(request, *args, **kwargs)
            # 获取新的refresh token
            new_refresh_token = response.data.get('refresh')
            if new_refresh_token:
                response.set_cookie(
                    key='refresh_token',
                    value=new_refresh_token,
                    httponly=True,
                    secure=True,
                    samesite='Lax',
                    max_age=2 * 24 * 3600
                )
                del response.data['refresh']
            return response
        except (InvalidToken, TokenError):
            return Response({'detail': 'Invalid or expired refresh token'}, status=status.HTTP_401_UNAUTHORIZED)

3. 修改LogoutView从Cookie获取Refresh Token

前端无需传递refresh token,直接从Cookie中读取并加入黑名单:

class LogoutView(generics.GenericAPIView):
    def post(self, request):
        refresh_token = request.COOKIES.get('refresh_token')
        if not refresh_token:
            return Response({'detail': 'No refresh token found'}, status=status.HTTP_400_BAD_REQUEST)
        try:
            token = RefreshToken(refresh_token)
            token.blacklist()
            # 清除Cookie
            response = Response({'Logout': 'Successfully'}, status=status.HTTP_200_OK)
            response.delete_cookie('refresh_token')
            return response
        except TokenError:
            return Response({'detail': 'Invalid refresh token'}, status=status.HTTP_400_BAD_REQUEST)

4. 更新urls.py

替换默认的TokenRefreshView为我们重写的版本:

path('api/login/', MyTokenObtainPairView.as_view(), name="token_obtain_pair"),
    path('api/token/refresh/', MyTokenRefreshView.as_view(), name="token_refresh"),
    path('api/register/', RegisterView.as_view(), name="auth_register"),
    path('api/logout/', LogoutView.as_view(), name="logout"),

5. 配置CORS支持跨域Cookie传递

因为React前端和Django后端跨域,需要在settings.py中配置CORS:

# 先安装依赖:pip install django-cors-headers
INSTALLED_APPS = [
    # ...其他已安装app
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 放在最前面
    'django.middleware.common.CommonMiddleware',
    # ...其他中间件
]

# 允许前端域名,比如React运行在http://localhost:3000
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    # 生产环境替换为实际前端域名
]

# 允许携带Cookie
CORS_ALLOW_CREDENTIALS = True

6. React前端适配

发送请求时需要设置credentials: 'include',让浏览器自动携带Cookie:

// 登录请求
fetch('http://localhost:8000/api/login/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  credentials: 'include',
  body: JSON.stringify({username: 'yourname', password: 'yourpass'}),
})
.then(res => res.json())
.then(data => {
  // 将access token存在内存(不建议存localStorage)
  sessionStorage.setItem('access_token', data.access);
});

// 刷新token请求
fetch('http://localhost:8000/api/token/refresh/', {
  method: 'POST',
  credentials: 'include',
})
.then(res => res.json())
.then(data => {
  sessionStorage.setItem('access_token', data.access);
});

// 登出请求
fetch('http://localhost:8000/api/logout/', {
  method: 'POST',
  credentials: 'include',
})
.then(res => res.json())
.then(data => {
  sessionStorage.removeItem('access_token');
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 14:45:26