Files
chunyu_project/user/views/token.py
T
root 3618323192 fix(security): P1/P2 审计修复 + JWT HttpOnly Cookie 双模认证 + 限流
- P0/P1 审计修复: 滑块验证码不再下发 x_position/成败即销毁 key、
  user-login 补失败计数+滑块门控、限流标识改 X-Real-IP、
  百度翻译 appkey 环境化、ChangeEmail/ChangePhone 补调 avalidate、
  logs/tasks.py Count(filter=Q) 修复、chat 收藏 SSRF 内网黑名单
- P1 #6/7: token_blacklist + ROTATE_REFRESH_TOKENS 开启,
  /user/token/refresh/ 挂载
- #2 JWT HttpOnly Cookie 双模认证: user/cookie_auth.py 种/清 Cookie,
  user/authentication.py CookieOrHeaderJWTAuthentication(Bearer 优先/_COOKIE 兜底),
  user/views/token.py CookieTokenRefreshView + UserLogoutAPIView(/user/logout/),
  create_standardized_response 自动对含 token 的响应种 Cookie,
  异步视图内 RefreshToken.for_user 全部 sync_to_async 包裹(修 SynchronousOnlyOperation 500),
  WS ChatConsumer 优先读 Cookie token
- P2 #11 限流: utils/rate_limit.py 固定窗口频控,
  shorturl 生成 匿名10次/分+登录60次/分, 邮箱验证码 同邮箱60s1次+同IP10次/10min,
  登录/注册验证码 错5次作废+成功即销毁防重放, 换绑邮箱/手机 同步落地,
  urls.py 补挂 shorturl 路由(此前 404)
2026-09-08 11:28:00 +08:00

69 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from rest_framework_simplejwt.views import TokenRefreshView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.response import Response
from rest_framework import status
from adrf.views import APIView
from rest_framework.permissions import AllowAny
from asgiref.sync import sync_to_async
from drf_yasg.utils import swagger_auto_schema
from chunyu_project.common_schemas import success_response
from utils.response_codes import create_standardized_response
from ..cookie_auth import set_auth_cookies, clear_auth_cookies
class CookieTokenRefreshView(TokenRefreshView):
"""
双模 Token 刷新端点:
- 支持从请求体 { refresh: '...' } 获取(移动端/API)
- 也支持从 HttpOnly Cookie 获取 refresh_token(Web 端)
- 刷新成功后,自动将新 access 与 refresh 写入 HttpOnly Cookie
"""
def post(self, request, *args, **kwargs):
# 若请求体中未传递 refresh,尝试从 Cookie 自动填充
has_refresh_in_body = bool(request.data.get('refresh')) if hasattr(request, 'data') and request.data else False
if not has_refresh_in_body and 'refresh_token' in request.COOKIES:
data = request.data.copy() if hasattr(request.data, 'copy') else dict(request.data or {})
data['refresh'] = request.COOKIES['refresh_token']
request._full_data = data
response = super().post(request, *args, **kwargs)
if response.status_code == status.HTTP_200_OK and isinstance(response.data, dict):
access = response.data.get('access')
refresh = response.data.get('refresh')
set_auth_cookies(response, access_token=access, refresh_token=refresh)
return response
class UserLogoutAPIView(APIView):
"""
用户登出端点:清除客户端 HttpOnly Cookie,并将 refresh token 放入黑名单(若有)
"""
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['认证'],
operation_summary='退出登录',
operation_description='清除 HttpOnly Cookie 并拉黑 refresh token',
responses={200: success_response},
)
async def post(self, request):
refresh = request.data.get('refresh') if hasattr(request, 'data') and request.data else None
if not refresh:
refresh = request.COOKIES.get('refresh_token')
if refresh:
def _blacklist(r):
try:
RefreshToken(r).blacklist()
except Exception:
pass
await sync_to_async(_blacklist)(refresh)
response = create_standardized_response(
data={'logged_out': True},
message='退出成功',
status_code=status.HTTP_200_OK
)
clear_auth_cookies(response)
return response