Files
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

117 lines
4.2 KiB
Python

from rest_framework import status
from adrf.views import APIView
from rest_framework.permissions import IsAuthenticated
from asgiref.sync import sync_to_async
from utils.response_codes import (
ResponseCode,
create_standardized_response,
create_standardized_error_response
)
from ..serializers.user_serializers import (
SendPhoneCodeSerializer,
ChangePhoneSerializer,
UserSerializer,
)
class SendPhoneCodeAPIView(APIView):
permission_classes = [IsAuthenticated]
async def post(self, request):
from utils.rate_limit import check_rate_limit
identifier = str(request.user.id)
if not await sync_to_async(check_rate_limit)('change_phone_send', identifier, limit=1, window_seconds=60):
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message="短信验证码发送过于频繁,请60秒后再试",
status_code=status.HTTP_429_TOO_MANY_REQUESTS
)
serializer = SendPhoneCodeSerializer(
data=request.data,
context={'request': request}
)
# 校验/save 内含 cache 写入,线程池兜底
if not await sync_to_async(serializer.is_valid)():
errors = serializer.errors
first_error = ''
for field, msgs in errors.items():
if isinstance(msgs, list) and msgs:
first_error = str(msgs[0])
break
return create_standardized_error_response(
data=errors,
code=ResponseCode.PARAMETER_ERROR,
message=first_error or '参数异常',
status_code=status.HTTP_400_BAD_REQUEST
)
try:
phone = await serializer.asave()
return create_standardized_response(
data={'phone_sent': True},
code=ResponseCode.SUCCESS,
status_code=status.HTTP_200_OK
)
except Exception as e:
return create_standardized_error_response(
message=f'验证码发送失败: {str(e)}',
code=ResponseCode.SERVER_INTERNAL_ERROR,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ChangePhoneAPIView(APIView):
permission_classes = [IsAuthenticated]
async def post(self, request):
serializer = ChangePhoneSerializer(
data=request.data,
context={'request': request}
)
# 校验/save 内含 cache 读写与 user.save() ORM 调用,线程池兜底
if not await sync_to_async(serializer.is_valid)():
errors = serializer.errors
first_error = ''
for field, msgs in errors.items():
if isinstance(msgs, list) and msgs:
first_error = str(msgs[0])
break
return create_standardized_error_response(
data=errors,
code=ResponseCode.PARAMETER_ERROR,
message=first_error or '参数异常',
status_code=status.HTTP_400_BAD_REQUEST
)
# 安全修复:此前从未调用 avalidate,短信验证码校验为死代码(可无码改绑手机号)
try:
await serializer.avalidate(serializer.validated_data)
except Exception as e:
return create_standardized_error_response(
data=getattr(e, 'detail', None) or {'code': [str(e)]},
code=ResponseCode.PARAMETER_ERROR,
message='验证码校验失败',
status_code=status.HTTP_400_BAD_REQUEST
)
try:
updated_user = await serializer.asave()
user_serializer = UserSerializer(updated_user)
return create_standardized_response(
data={'user': await user_serializer.adata},
code=ResponseCode.SUCCESS,
status_code=status.HTTP_200_OK
)
except Exception as e:
return create_standardized_error_response(
message=f'手机号修改失败: {str(e)}',
code=ResponseCode.SERVER_INTERNAL_ERROR,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)