如何在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
相关产品推荐
相关产品推荐

