- 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)
61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
from .slider_captcha import (
|
|
generate_slider_captcha as _base_generate,
|
|
verify_slider_captcha as _base_verify,
|
|
verify_slider_trajectory as _base_verify_traj,
|
|
SliderCaptchaError,
|
|
)
|
|
|
|
from .rate_limit import check_rate_limit
|
|
|
|
# 频控阈值:单 IP 10 秒内最多 6 次生成(普通用户基本用不到这个量)
|
|
SLIDER_GENERATE_LIMIT = 6
|
|
SLIDER_GENERATE_WINDOW = 10 # 秒
|
|
|
|
# 验证失败计数:单 IP 5 分钟内累计 10 次失败即拒绝继续验证,
|
|
# 防止对单 captcha_key 失败重试耗尽缓存/拖慢接口
|
|
SLIDER_VERIFY_FAIL_LIMIT = 10
|
|
SLIDER_VERIFY_FAIL_WINDOW = 300 # 秒
|
|
|
|
|
|
def _client_ip(request) -> str:
|
|
fwd = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if fwd:
|
|
return fwd.split(',')[0].strip()
|
|
return request.META.get('REMOTE_ADDR', '')
|
|
|
|
|
|
def generate_slider_captcha(request=None):
|
|
if request is not None:
|
|
ip = _client_ip(request)
|
|
if ip and not check_rate_limit('slider_gen', ip, SLIDER_GENERATE_LIMIT, SLIDER_GENERATE_WINDOW):
|
|
from utils.response_codes import ResponseCode, create_standardized_error_response
|
|
from rest_framework import status
|
|
return create_standardized_error_response(
|
|
code=ResponseCode.RATE_LIMITED,
|
|
message='请求过于频繁,请稍后再试',
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
)
|
|
return _base_generate()
|
|
|
|
|
|
def verify_slider_captcha(captcha_key: str, x_position: int, request=None) -> bool:
|
|
ok = _base_verify(captcha_key, x_position)
|
|
if request is not None and not ok:
|
|
ip = _client_ip(request)
|
|
if ip:
|
|
check_rate_limit('slider_fail', ip, SLIDER_VERIFY_FAIL_LIMIT, SLIDER_VERIFY_FAIL_WINDOW)
|
|
return ok
|
|
|
|
|
|
def verify_slider_trajectory(captcha_key: str, x_position: int, trajectory, request=None) -> bool:
|
|
ok = _base_verify_traj(captcha_key, x_position, trajectory)
|
|
if request is not None and not ok:
|
|
ip = _client_ip(request)
|
|
if ip:
|
|
check_rate_limit('slider_fail', ip, SLIDER_VERIFY_FAIL_LIMIT, SLIDER_VERIFY_FAIL_WINDOW)
|
|
return ok
|
|
|
|
|
|
def is_captcha_error(err) -> bool:
|
|
return isinstance(err, SliderCaptchaError)
|