feat(smtp): 邮箱验证码连续校验失败 5 次自动作废,防枚举 6 位数字码
This commit is contained in:
1 parent
c5dd8d198d
commit
5275b8bf75
3 files changed
+23
-5
No files matched your search
@@ -136,7 +136,7 @@
|
||||
### smtp(邮件服务)
|
||||
|
||||
- 注册流程的邮箱验证码后端,基于 pymsg(MsgServer,TCP 长度前缀帧 + JSON),监听 127.0.0.1:8008。
|
||||
- pymsg 路由:`send_email`(生成 6 位验证码并经 SMTP_SSL 发送,10 分钟有效,发送失败自动作废;同一邮箱 60 秒发送冷却,冷却期拒绝并返回 `retry_after`,不覆盖已发出的有效验证码,发送失败不计入冷却)、`verify_email`(校验,通过即一次性消费防重放,兼容旧字段 `input_captcha`)、`health`。
|
||||
- pymsg 路由:`send_email`(生成 6 位验证码并经 SMTP_SSL 发送,10 分钟有效,发送失败自动作废;同一邮箱 60 秒发送冷却,冷却期拒绝并返回 `retry_after`,不覆盖已发出的有效验证码,发送失败不计入冷却)、`verify_email`(校验,通过即一次性消费防重放,连续失败 5 次自动作废并返回 `invalidated`,兼容旧字段 `input_captcha`)、`health`。
|
||||
- 验证码存于进程内存(锁保护 + 惰性过期清理),SMTP 账号通过 `.env` 配置(`SMTP_SERVER` / `SMTP_PORT` / `SMTP_USER` / `__PASSWORD`,`__PASSWORD` 兼容别名 `SMTP_PASSWORD`)。
|
||||
|
||||
### aibox(独立 AI 智能体系统)
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ UniServe Framework 的应用层网关,C++20 实现。
|
||||
{ "username": "user@example.com", "password": "...", "email_code": "123456" }
|
||||
```
|
||||
|
||||
校验顺序:密码强度 → 邮箱格式 → CAPTCHA(可选)→ **邮箱验证码**(经 pymsg 调 smtp:8008 `verify_email`,一次性消费,fail-closed),全部通过后写入 `Users` 表。
|
||||
校验顺序:密码强度 → 邮箱格式 → CAPTCHA(可选)→ **邮箱验证码**(经 pymsg 调 smtp:8008 `verify_email`,一次性消费,连续失败 5 次自动作废,fail-closed),全部通过后写入 `Users` 表。
|
||||
|
||||
错误码:`400`(密码/邮箱/CAPTCHA/邮箱验证码不合规)、`429`(速率超限)、`500`(数据库异常)。
|
||||
|
||||
|
||||
+21
-3
@@ -73,6 +73,7 @@ SMTP_PASSWORD = os.environ.get("__PASSWORD") or os.environ.get("SMTP_PASSWORD",
|
||||
CODE_LEN = 6
|
||||
CODE_TTL = 600 # 验证码有效期(秒)
|
||||
SEND_INTERVAL = 60 # 同一邮箱两次成功发送的最小间隔(秒)
|
||||
MAX_VERIFY_ATTEMPTS = 5 # 同一验证码最大校验失败次数,达到即作废(防枚举 6 位数字码)
|
||||
# 多数邮箱服务商(163/QQ 等)强制 From 头与登录账号一致,故默认跟随 SMTP_USER;
|
||||
# 仅当服务商允许自定义发件身份时才在 .env 里显式配 MAIL_FROM 覆盖
|
||||
MAIL_FROM = os.environ.get("MAIL_FROM") or SMTP_USER
|
||||
@@ -80,7 +81,7 @@ DEFAULT_SUBJECT = "DNFS 邮箱验证"
|
||||
# 有效期文案由 CODE_TTL 派生,改 TTL 不用同步改这里({{code}} 是 f-string 转义)
|
||||
DEFAULT_CONTENT = f"DNFS: 您的邮箱验证码为 {{code}},{CODE_TTL // 60} 分钟内有效。"
|
||||
|
||||
code_store = {} # email -> {'code': str, 'expires_at': float}
|
||||
code_store = {} # email -> {'code': str, 'expires_at': float, 'attempts': int}
|
||||
send_history = {} # email -> 上次成功发送的时间戳(发送冷却用)
|
||||
STORE_LOCK = threading.Lock()
|
||||
|
||||
@@ -187,7 +188,9 @@ class SmtpServer:
|
||||
content = str(req.get("content") or DEFAULT_CONTENT).replace("{code}", code)
|
||||
|
||||
with STORE_LOCK:
|
||||
code_store[email] = {'code': code, 'expires_at': time.time() + CODE_TTL}
|
||||
code_store[email] = {'code': code,
|
||||
'expires_at': time.time() + CODE_TTL,
|
||||
'attempts': 0}
|
||||
# 惰性清理过期项
|
||||
now = time.time()
|
||||
expired = [k for k, v in code_store.items() if v['expires_at'] < now]
|
||||
@@ -223,13 +226,18 @@ class SmtpServer:
|
||||
return {"sent": True, "ttl": CODE_TTL}
|
||||
|
||||
def _handle_verify_email(self, req):
|
||||
"""校验验证码,验证通过即一次性消费;不存在的 email 安全返回 invalid。"""
|
||||
"""校验验证码,验证通过即一次性消费;不存在的 email 安全返回 invalid。
|
||||
|
||||
同一验证码连续校验失败达 MAX_VERIFY_ATTEMPTS 次即作废删除(响应带
|
||||
invalidated 标记),防止对 6 位数字码的在线枚举。
|
||||
"""
|
||||
email = str(req.get("email", "")).strip()
|
||||
# 兼容旧字段 input_captcha
|
||||
code = str(req.get("code") or req.get("input_captcha") or "").strip()
|
||||
if not email or not code:
|
||||
return {"valid": False, "error": "email and code required"}
|
||||
|
||||
invalidated = False
|
||||
with STORE_LOCK:
|
||||
entry = code_store.get(email)
|
||||
if entry and entry['expires_at'] < time.time():
|
||||
@@ -241,10 +249,20 @@ class SmtpServer:
|
||||
valid = True
|
||||
else:
|
||||
valid = False
|
||||
if entry:
|
||||
# 仅对已发出且未过期的验证码计失败次数;达到上限即作废
|
||||
entry['attempts'] = entry.get('attempts', 0) + 1
|
||||
if entry['attempts'] >= MAX_VERIFY_ATTEMPTS:
|
||||
del code_store[email]
|
||||
invalidated = True
|
||||
|
||||
if valid:
|
||||
logs.info("smtp", f"{email}: 验证通过")
|
||||
return {"valid": True}
|
||||
if invalidated:
|
||||
logs.info("smtp", f"{email}: 连续校验失败 {MAX_VERIFY_ATTEMPTS} 次,验证码已作废")
|
||||
return {"valid": False, "invalidated": True,
|
||||
"error": f"验证码连续错误 {MAX_VERIFY_ATTEMPTS} 次,已失效,请重新获取"}
|
||||
logs.info("smtp", f"{email}: 验证失败")
|
||||
return {"valid": False, "error": "invalid or expired code"}
|
||||
|
||||
|
||||
Reference in new issue
Block a user