# verify_code_engine.py
# ==========================================
# 📧 验证码发送引擎
# ==========================================
# 作用:提供验证码内存缓存与多渠道发送能力(邮件/短信)
# 关联文件:
# - router_users_auth.py (调用此模块发送验证码)
# - router_wallet.py (提现时也会调用验证码缓存)
# ==========================================
import os
import json
import time
import urllib.request
import urllib.parse
import base64
from fastapi import HTTPException
# ==========================================
# 验证码内存缓存 (全局共享字典)
# ==========================================
# 作用:存储所有待验证的验证码,格式为 {key: {code, expires_at}}
# 关联:router_users_auth.py 和 router_wallet.py 都会读写此字典
# 注意:服务重启后缓存会清空,生产环境建议改用 Redis
VERIFY_CODES = {}
# 🔒 发送频率限制配置
SEND_COOLDOWN = {} # {contact: last_send_timestamp}
COOLDOWN_SECONDS = 60 # 同一联系方式60秒内最多发送1条验证码
# 🚀 P1性能优化:记录上次清理时间,避免频繁清理
_last_cleanup_time = 0
_CLEANUP_INTERVAL = 300 # 5分钟清理一次
def check_send_cooldown(contact: str):
"""
🔒 检查发送频率限制
作用:防止同一联系方式频繁发送验证码
参数:
- contact: 联系方式(邮箱或手机号)
抛出:
- HTTPException: 如果在冷却期内,返回429状态码
"""
now = time.time()
last_send = SEND_COOLDOWN.get(contact, 0)
if now - last_send < COOLDOWN_SECONDS:
remaining = int(COOLDOWN_SECONDS - (now - last_send))
raise HTTPException(status_code=429, detail=f"请等待{remaining}秒后再发送验证码")
SEND_COOLDOWN[contact] = now
def cleanup_expired_codes():
"""
🚀 P1性能优化:清理过期验证码,防止内存泄漏
🔒 同时清理过期的发送频率限制记录
"""
global _last_cleanup_time
now = time.time()
# 限制清理频率,避免每次请求都清理
if now - _last_cleanup_time < _CLEANUP_INTERVAL:
return
_last_cleanup_time = now
# 收集过期的验证码键
expired_keys = [
key for key, data in VERIFY_CODES.items()
if now > data.get("expires_at", data.get("expires", 0))
]
# 删除过期验证码
for key in expired_keys:
del VERIFY_CODES[key]
# 🔒 清理过期的发送频率限制记录,防止内存泄漏
expired_cooldown_contacts = [
contact for contact, last_send in SEND_COOLDOWN.items()
if now - last_send >= COOLDOWN_SECONDS
]
for contact in expired_cooldown_contacts:
del SEND_COOLDOWN[contact]
total_cleaned = len(expired_keys) + len(expired_cooldown_contacts)
if total_cleaned > 0:
print(f"🧹 已清理 {len(expired_keys)} 个过期验证码,{len(expired_cooldown_contacts)} 个过期冷却记录,当前缓存: {len(VERIFY_CODES)} 条验证码,{len(SEND_COOLDOWN)} 条冷却记录")
def send_email_code(to_email: str, code: str, action: str):
"""
📧 邮件验证码发送函数
作用:通过 Make.com Webhook 触发邮件发送(无需 SMTP 服务器)
参数:
- to_email: 目标邮箱地址
- code: 6位数字验证码
- action: 动作类型 ("register" 注册 / "reset" 重置密码 / "withdraw" 提现)
关联:
- 环境变量 MAKE_WEBHOOK_URL (在 HF Space Settings 中配置)
- router_users_auth.py 的 send_verify_code() 异步调用此函数
"""
# 从环境变量读取 Make.com Webhook 地址
webhook_url = os.environ.get("MAKE_WEBHOOK_URL")
if not webhook_url:
print("警告: 未配置 MAKE_WEBHOOK_URL,跳过邮件发送")
return
# 根据动作类型生成不同的邮件标题
if action == "register":
action_str = "注册账号"
elif action == "withdraw":
action_str = "提现操作"
else:
action_str = "修改/找回密码"
subject = f"ComfyUI 社区 - {action_str}验证码"
# 构建 HTML 格式的邮件正文(美化样式)
html_content = f"""
ComfyUI 社区精选
您好,
您正在请求{action_str},您的验证码是:
{code}
该验证码在 5 分钟内有效。如非本人操作,请忽略此邮件。
"""
# 组装 Webhook 请求载荷
data = {
"to": to_email,
"subject": subject,
"html": html_content
}
# 发送 HTTP POST 请求触发 Webhook
req = urllib.request.Request(
webhook_url,
data=json.dumps(data).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
try:
with urllib.request.urlopen(req, timeout=10) as response:
print(f"✅ 成功触发 Webhook 发送验证码 {code} 至 {to_email}")
except Exception as e:
print(f"❌ Webhook 触发失败: {e}")
def send_sms_code(phone: str, code: str, action: str):
"""
📱 短信验证码发送函数(双引擎:Twilio + 阿里云)
作用:支持海外用户(Twilio)和国内用户(阿里云)的短信发送
参数:
- phone: 手机号(需带国际区号,如 +86)
- code: 6位数字验证码
- action: 动作类型 ("register" 注册 / "reset" 重置密码 / "withdraw" 提现)
关联:
- 环境变量 TWILIO_SID, TWILIO_TOKEN, TWILIO_FROM (Twilio配置)
- 环境变量 ALIYUN_AK, ALIYUN_SK, ALIYUN_SIGN_NAME (阿里云配置)
- router_users_auth.py 的 send_verify_code() 异步调用此函数
"""
if action == "register":
action_str = "注册账号"
elif action == "withdraw":
action_str = "提现操作"
else:
action_str = "修改/找回密码"
# ==========================================
# 引擎 A:Twilio (海外优先,无需SDK,纯HTTP接口)
# ==========================================
# 作用:为海外用户或测试环境提供短信能力
# 关联:需在 HF Space Settings 配置 TWILIO_SID, TWILIO_TOKEN, TWILIO_FROM
twilio_sid = os.environ.get("TWILIO_SID")
if twilio_sid:
token = os.environ.get("TWILIO_TOKEN")
from_phone = os.environ.get("TWILIO_FROM")
# 构建短信内容
body = f"【ComfyUI社区】您正在请求{action_str},验证码是:{code},5分钟内有效。"
url = f"https://api.twilio.com/2010-04-01/Accounts/{twilio_sid}/Messages.json"
# Twilio 使用 Basic Auth 认证
auth = base64.b64encode(f"{twilio_sid}:{token}".encode('utf-8')).decode('utf-8')
data = urllib.parse.urlencode({'To': phone, 'From': from_phone, 'Body': body}).encode('utf-8')
req = urllib.request.Request(url, data=data)
req.add_header("Authorization", f"Basic {auth}")
try:
with urllib.request.urlopen(req, timeout=10) as response:
print(f"✅ Twilio 短信已成功下发至 {phone}")
except Exception as e:
print(f"❌ Twilio 发送失败: {e}")
return
# ==========================================
# 引擎 B:阿里云 (国内首选,到达率最高)
# ==========================================
# 作用:为国内用户提供短信能力,需要已备案的签名和模板
# 关联:需在 HF Space Settings 配置 ALIYUN_AK, ALIYUN_SK 等
aliyun_ak = os.environ.get("ALIYUN_AK")
if aliyun_ak:
# 阿里云SDK需要单独安装,这里做动态导入
try:
from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
except ImportError:
print("❌ 缺少阿里云 SDK,请在 requirements.txt 中添加 alibabacloud_dysmsapi20170525")
return
sk = os.environ.get("ALIYUN_SK")
sign_name = os.environ.get("ALIYUN_SIGN_NAME") # 短信签名,如 "阿里云"
# 根据动作类型选择不同的模板
tpl_code = os.environ.get("ALIYUN_TPL_REGISTER") if action == "register" else os.environ.get("ALIYUN_TPL_RESET")
# 初始化阿里云客户端
config = open_api_models.Config(access_key_id=aliyun_ak, access_key_secret=sk)
config.endpoint = 'dysmsapi.aliyuncs.com'
client = DysmsapiClient(config)
# 构建发送请求
send_req = dysmsapi_models.SendSmsRequest(
phone_numbers=phone,
sign_name=sign_name,
template_code=tpl_code,
template_param=json.dumps({"code": code}) # 模板参数
)
try:
response = client.send_sms(send_req)
if response.body.code == "OK":
print(f"✅ 阿里云短信已成功下发至 {phone}")
else:
print(f"❌ 阿里云下发失败: {response.body.message}")
except Exception as e:
print(f"❌ 阿里云请求异常: {e}")
return
# ==========================================
# 降级模式:控制台打印模拟
# ==========================================
# 作用:当没有配置任何短信服务时,仅在控制台打印验证码(仅限开发测试)
print(f"⚠️ 未配置短信秘钥,模拟下发 -> 手机号: {phone}, 验证码: {code}")