- 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)
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
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
|