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)
This commit is contained in:
2026-09-08 11:28:00 +08:00
parent 2944b19e6f
commit 3618323192
22 changed files with 711 additions and 37 deletions
+77 -1
View File
@@ -9,7 +9,7 @@ from learn.models import Course, CourseFavorite
from apidirectory.models import ApiItem, ApiFavorite
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from chunyu_project.common_schemas import success_response, error_response, unauthorized_response
from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response
class MyFavoritesView(APIView):
@@ -114,3 +114,79 @@ class MyFavoritesView(APIView):
data={'results': results, 'counts': counts},
code=ResponseCode.SUCCESS
)
class FavoriteToggleView(APIView):
"""统一收藏切换端点:按 type + id 切换 tool/article/course/api 收藏状态。
修复:Android 端 FavoritesApi.toggleFavorite 调用的 user/favorites/toggle/
此前不存在(404)。响应契约与客户端对齐:{is_favorited, favorites_count}。
"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['收藏'],
operation_summary='切换收藏状态',
operation_description='按类型切换收藏(tool/article/course/api),返回切换后的状态',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['type', 'target_id'],
properties={
'type': openapi.Schema(type=openapi.TYPE_STRING, enum=['tool', 'article', 'course', 'api']),
'target_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='目标对象 ID'),
},
),
responses={200: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response},
)
async def post(self, request):
fav_type = request.data.get('type')
target_id = request.data.get('target_id')
if fav_type not in ('tool', 'article', 'course', 'api'):
return create_standardized_error_response(
message='type 必须为 tool/article/course/api 之一',
code=ResponseCode.PARAMETER_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
target_id = int(target_id)
except (TypeError, ValueError):
return create_standardized_error_response(
message='target_id 必须为整数',
code=ResponseCode.PARAMETER_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
# (模型, FK 字段名, 目标模型)
mapping = {
'tool': (ToolFavorite, 'tool', Tool),
'article': (ArticleFavorite, 'article', Article),
'course': (CourseFavorite, 'course', Course),
'api': (ApiFavorite, 'api_item', ApiItem),
}
model, fk_field, target_model = mapping[fav_type]
try:
await target_model.objects.aget(id=target_id)
except target_model.DoesNotExist:
return create_standardized_error_response(
message='目标对象不存在',
code=ResponseCode.PARAMETER_ERROR,
status_code=status.HTTP_404_NOT_FOUND
)
existing = await model.objects.filter(user=request.user, **{fk_field: target_id}).afirst()
if existing is not None:
await model.objects.filter(user=request.user, **{fk_field: target_id}).adelete()
favorited = False
else:
await model.objects.acreate(user=request.user, **{fk_field: target_id})
favorited = True
favorites_count = await model.objects.filter(user=request.user).acount()
return create_standardized_response(
data={'is_favorited': favorited, 'favorites_count': favorites_count, 'type': fav_type, 'target_id': target_id},
code=ResponseCode.SUCCESS
)