feat(auth): 短信登录防刷 — 同手机号同IP 每小时限 5 次
/sms/login 增加 (手机号+IP) 每小时最多 5 次限流,挡撞库爆破/高频重登; 与路由上原有 IP 维度限流(20次/分钟,跨号)互补。 - 计数放在验证码校验之前 → 输错码的失败尝试也计数 - 测试账号(.env TEST_ACCOUNT_PHONE)豁免,走自己的每日额度 - 复用 ratelimit 内存固定窗口,受 RATE_LIMIT_ENABLED 总开关控制(默认开) - 新增 enforce_rate_limit(供路由内按请求体字段如手机号限流) - 补 test_auth/test_test_account 回归测试(普通号到5次被拦、测试号豁免) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-3
@@ -12,11 +12,11 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
||||
from app.api.deps import CurrentUser, DbSession
|
||||
from app.core import test_account
|
||||
from app.core.ratelimit import rate_limit
|
||||
from app.core.ratelimit import enforce_rate_limit, rate_limit
|
||||
from app.core.security import TokenError, decode_token, issue_token_pair
|
||||
from app.integrations.jiguang import JiguangError, mask_phone, verify_and_get_phone
|
||||
from app.integrations.sms import SmsError, send_code, verify_code
|
||||
@@ -38,6 +38,9 @@ logger = logging.getLogger("shagua.auth")
|
||||
|
||||
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
|
||||
# 手机号登录防刷:同一手机号 + 同一 IP 每小时最多的登录尝试次数(成功/失败都计)。
|
||||
SMS_LOGIN_MAX_PER_HOUR = 5
|
||||
|
||||
|
||||
def _login_response(
|
||||
user, *, onboarding_completed: bool, force_onboarding: bool = False
|
||||
@@ -109,9 +112,10 @@ def sms_send(req: SmsSendRequest) -> SmsSendResponse:
|
||||
summary="手机号+验证码登录",
|
||||
dependencies=[Depends(rate_limit(20, 60, "sms-login"))], # 防撞库爆破(另有单码失败次数上限)
|
||||
)
|
||||
def sms_login(req: SmsLoginRequest, db: DbSession) -> TokenWithUser:
|
||||
def sms_login(req: SmsLoginRequest, request: Request, db: DbSession) -> TokenWithUser:
|
||||
# 测试账号:免验证码登录 + 每日上限 + 每次都走新手引导(详见 app/core/test_account.py)。
|
||||
# 放在最前面:命中即不校验验证码;先扣当日额度,超限直接拒,挡住有人猜到号后脚本刷。
|
||||
# 测试账号走自己的每日额度、不受下面 (手机号+IP) 每小时限流约束(QA 需在一小时内反复登录联调)。
|
||||
if test_account.is_test_account(req.phone):
|
||||
if not test_account.try_consume_quota():
|
||||
raise HTTPException(status_code=429, detail="测试账号今日使用次数已达上限,请明天再试")
|
||||
@@ -123,6 +127,17 @@ def sms_login(req: SmsLoginRequest, db: DbSession) -> TokenWithUser:
|
||||
# 不读/不写 onboarding_completion 表。
|
||||
return _login_response(user, onboarding_completed=False, force_onboarding=True)
|
||||
|
||||
# 防刷:同一手机号 + 同一 IP 每小时最多 SMS_LOGIN_MAX_PER_HOUR 次登录尝试。放在验证码校验**之前**
|
||||
# → 输错验证码的失败尝试也计数,才挡得住撞库/爆破。与路由上 IP 维度的 sms-login 限流(同 IP 多号)互补。
|
||||
enforce_rate_limit(
|
||||
request,
|
||||
scope="sms-login-phone",
|
||||
subject=req.phone,
|
||||
limit=SMS_LOGIN_MAX_PER_HOUR,
|
||||
window_sec=3600,
|
||||
detail="登录尝试过于频繁,请稍后再试",
|
||||
)
|
||||
|
||||
if not verify_code(req.phone, req.code):
|
||||
raise HTTPException(status_code=400, detail="invalid sms code")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user