Spaces:
Running
Running
Upload 20 files
Browse files- requirements.txt +3 -1
- verify_code_engine.py +39 -5
- 安全认证.py +31 -23
requirements.txt
CHANGED
|
@@ -12,4 +12,6 @@ httpx==0.25.2
|
|
| 12 |
python-alipay-sdk==3.1.0
|
| 13 |
aiofiles==23.2.1
|
| 14 |
# 🚀 P2优化:速率限制
|
| 15 |
-
slowapi==0.1.9
|
|
|
|
|
|
|
|
|
| 12 |
python-alipay-sdk==3.1.0
|
| 13 |
aiofiles==23.2.1
|
| 14 |
# 🚀 P2优化:速率限制
|
| 15 |
+
slowapi==0.1.9
|
| 16 |
+
# 🔒 P0安全增强:bcrypt密码哈希
|
| 17 |
+
bcrypt==4.1.2
|
verify_code_engine.py
CHANGED
|
@@ -15,6 +15,8 @@ import urllib.request
|
|
| 15 |
import urllib.parse
|
| 16 |
import base64
|
| 17 |
|
|
|
|
|
|
|
| 18 |
# ==========================================
|
| 19 |
# 验证码内存缓存 (全局共享字典)
|
| 20 |
# ==========================================
|
|
@@ -23,14 +25,37 @@ import base64
|
|
| 23 |
# 注意:服务重启后缓存会清空,生产环境建议改用 Redis
|
| 24 |
VERIFY_CODES = {}
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
# 🚀 P1性能优化:记录上次清理时间,避免频繁清理
|
| 27 |
_last_cleanup_time = 0
|
| 28 |
_CLEANUP_INTERVAL = 300 # 5分钟清理一次
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def cleanup_expired_codes():
|
| 32 |
"""
|
| 33 |
🚀 P1性能优化:清理过期验证码,防止内存泄漏
|
|
|
|
| 34 |
"""
|
| 35 |
global _last_cleanup_time
|
| 36 |
now = time.time()
|
|
@@ -41,7 +66,7 @@ def cleanup_expired_codes():
|
|
| 41 |
|
| 42 |
_last_cleanup_time = now
|
| 43 |
|
| 44 |
-
# 收集过期的键
|
| 45 |
expired_keys = [
|
| 46 |
key for key, data in VERIFY_CODES.items()
|
| 47 |
if now > data.get("expires_at", data.get("expires", 0))
|
|
@@ -51,8 +76,17 @@ def cleanup_expired_codes():
|
|
| 51 |
for key in expired_keys:
|
| 52 |
del VERIFY_CODES[key]
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def send_email_code(to_email: str, code: str, action: str):
|
|
@@ -87,7 +121,7 @@ def send_email_code(to_email: str, code: str, action: str):
|
|
| 87 |
<p>您好,</p>
|
| 88 |
<p>您正在请求<strong>{action_str}</strong>,您的验证码是:</p>
|
| 89 |
<div style="font-size:24px; font-weight:bold; color:#2196F3; background:#e3f2fd; padding:15px; text-align:center; border-radius:6px; letter-spacing: 5px;">{code}</div>
|
| 90 |
-
<p style="color:#888; font-size:12px; margin-top:20px;">该验证码在
|
| 91 |
</div>
|
| 92 |
</div>
|
| 93 |
"""
|
|
@@ -139,7 +173,7 @@ def send_sms_code(phone: str, code: str, action: str):
|
|
| 139 |
from_phone = os.environ.get("TWILIO_FROM")
|
| 140 |
|
| 141 |
# 构建短信内容
|
| 142 |
-
body = f"【ComfyUI社区】您正在请求{action_str},验证码是:{code},
|
| 143 |
url = f"https://api.twilio.com/2010-04-01/Accounts/{twilio_sid}/Messages.json"
|
| 144 |
|
| 145 |
# Twilio 使用 Basic Auth 认证
|
|
|
|
| 15 |
import urllib.parse
|
| 16 |
import base64
|
| 17 |
|
| 18 |
+
from fastapi import HTTPException
|
| 19 |
+
|
| 20 |
# ==========================================
|
| 21 |
# 验证码内存缓存 (全局共享字典)
|
| 22 |
# ==========================================
|
|
|
|
| 25 |
# 注意:服务重启后缓存会清空,生产环境建议改用 Redis
|
| 26 |
VERIFY_CODES = {}
|
| 27 |
|
| 28 |
+
# 🔒 发送频率限制配置
|
| 29 |
+
SEND_COOLDOWN = {} # {contact: last_send_timestamp}
|
| 30 |
+
COOLDOWN_SECONDS = 60 # 同一联系方式60秒内最多发送1条验证码
|
| 31 |
+
|
| 32 |
# 🚀 P1性能优化:记录上次清理时间,避免频繁清理
|
| 33 |
_last_cleanup_time = 0
|
| 34 |
_CLEANUP_INTERVAL = 300 # 5分钟清理一次
|
| 35 |
|
| 36 |
|
| 37 |
+
def check_send_cooldown(contact: str):
|
| 38 |
+
"""
|
| 39 |
+
🔒 检查发送频率限制
|
| 40 |
+
|
| 41 |
+
作用:防止同一联系方式频繁发送验证码
|
| 42 |
+
参数:
|
| 43 |
+
- contact: 联系方式(邮箱或手机号)
|
| 44 |
+
抛出:
|
| 45 |
+
- HTTPException: 如果在冷却期内,返回429状态码
|
| 46 |
+
"""
|
| 47 |
+
now = time.time()
|
| 48 |
+
last_send = SEND_COOLDOWN.get(contact, 0)
|
| 49 |
+
if now - last_send < COOLDOWN_SECONDS:
|
| 50 |
+
remaining = int(COOLDOWN_SECONDS - (now - last_send))
|
| 51 |
+
raise HTTPException(status_code=429, detail=f"请等待{remaining}秒后再发送验证码")
|
| 52 |
+
SEND_COOLDOWN[contact] = now
|
| 53 |
+
|
| 54 |
+
|
| 55 |
def cleanup_expired_codes():
|
| 56 |
"""
|
| 57 |
🚀 P1性能优化:清理过期验证码,防止内存泄漏
|
| 58 |
+
🔒 同时清理过期的发送频率限制记录
|
| 59 |
"""
|
| 60 |
global _last_cleanup_time
|
| 61 |
now = time.time()
|
|
|
|
| 66 |
|
| 67 |
_last_cleanup_time = now
|
| 68 |
|
| 69 |
+
# 收集过期的验证码键
|
| 70 |
expired_keys = [
|
| 71 |
key for key, data in VERIFY_CODES.items()
|
| 72 |
if now > data.get("expires_at", data.get("expires", 0))
|
|
|
|
| 76 |
for key in expired_keys:
|
| 77 |
del VERIFY_CODES[key]
|
| 78 |
|
| 79 |
+
# 🔒 清理过期的发送频率限制记录,防止内存泄漏
|
| 80 |
+
expired_cooldown_contacts = [
|
| 81 |
+
contact for contact, last_send in SEND_COOLDOWN.items()
|
| 82 |
+
if now - last_send >= COOLDOWN_SECONDS
|
| 83 |
+
]
|
| 84 |
+
for contact in expired_cooldown_contacts:
|
| 85 |
+
del SEND_COOLDOWN[contact]
|
| 86 |
+
|
| 87 |
+
total_cleaned = len(expired_keys) + len(expired_cooldown_contacts)
|
| 88 |
+
if total_cleaned > 0:
|
| 89 |
+
print(f"🧹 已清理 {len(expired_keys)} 个过期验证码,{len(expired_cooldown_contacts)} 个过期冷却记录,当前缓存: {len(VERIFY_CODES)} 条验证码,{len(SEND_COOLDOWN)} 条冷却记录")
|
| 90 |
|
| 91 |
|
| 92 |
def send_email_code(to_email: str, code: str, action: str):
|
|
|
|
| 121 |
<p>您好,</p>
|
| 122 |
<p>您正在请求<strong>{action_str}</strong>,您的验证码是:</p>
|
| 123 |
<div style="font-size:24px; font-weight:bold; color:#2196F3; background:#e3f2fd; padding:15px; text-align:center; border-radius:6px; letter-spacing: 5px;">{code}</div>
|
| 124 |
+
<p style="color:#888; font-size:12px; margin-top:20px;">该验证码在 5 分钟内有效。如非本人操作,请忽略此邮件。</p>
|
| 125 |
</div>
|
| 126 |
</div>
|
| 127 |
"""
|
|
|
|
| 173 |
from_phone = os.environ.get("TWILIO_FROM")
|
| 174 |
|
| 175 |
# 构建短信内容
|
| 176 |
+
body = f"【ComfyUI社区】您正在请求{action_str},验证码是:{code},5分钟内有效。"
|
| 177 |
url = f"https://api.twilio.com/2010-04-01/Accounts/{twilio_sid}/Messages.json"
|
| 178 |
|
| 179 |
# Twilio 使用 Basic Auth 认证
|
安全认证.py
CHANGED
|
@@ -18,6 +18,9 @@ import time
|
|
| 18 |
from typing import Optional, Tuple
|
| 19 |
from fastapi import HTTPException, Header
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# ==========================================
|
| 23 |
# 🔑 安全密钥配置
|
|
@@ -40,29 +43,25 @@ TOKEN_EXPIRE_SECONDS = 7 * 24 * 60 * 60
|
|
| 40 |
|
| 41 |
def hash_password(password: str) -> str:
|
| 42 |
"""
|
| 43 |
-
|
| 44 |
|
| 45 |
参数:
|
| 46 |
password: 用户输入的明文密码
|
| 47 |
|
| 48 |
返回:
|
| 49 |
-
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
"""
|
| 54 |
-
|
| 55 |
-
salted_password = f"{PASSWORD_SALT}{password}"
|
| 56 |
-
|
| 57 |
-
# SHA256 哈希
|
| 58 |
-
hash_obj = hashlib.sha256(salted_password.encode("utf-8"))
|
| 59 |
-
|
| 60 |
-
return hash_obj.hexdigest()
|
| 61 |
|
| 62 |
|
| 63 |
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 64 |
"""
|
| 65 |
-
验证
|
| 66 |
|
| 67 |
参数:
|
| 68 |
plain_password: 用户输入的明文密码
|
|
@@ -70,15 +69,26 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
| 70 |
|
| 71 |
返回:
|
| 72 |
True 匹配 / False 不匹配
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
"""
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
def require_password_match(stored_password: str, input_password: str) -> bool:
|
| 78 |
"""
|
| 79 |
-
统一
|
| 80 |
|
| 81 |
-
作用:封装密码验证逻辑,
|
| 82 |
|
| 83 |
参数:
|
| 84 |
stored_password: 数据库中存储的密码(哈希值)
|
|
@@ -88,19 +98,17 @@ def require_password_match(stored_password: str, input_password: str) -> bool:
|
|
| 88 |
True 密码匹配
|
| 89 |
|
| 90 |
异常:
|
| 91 |
-
HTTPException 401:
|
| 92 |
|
| 93 |
使用场景:
|
| 94 |
- 用户登录时验证密码
|
| 95 |
- 敏感操作前验证当前密码
|
| 96 |
"""
|
| 97 |
-
if
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
)
|
| 103 |
-
return verify_password(input_password, stored_password)
|
| 104 |
|
| 105 |
|
| 106 |
# ==========================================
|
|
|
|
| 18 |
from typing import Optional, Tuple
|
| 19 |
from fastapi import HTTPException, Header
|
| 20 |
|
| 21 |
+
# 🔒 P0安全增强:引入bcrypt进行密码哈希
|
| 22 |
+
import bcrypt
|
| 23 |
+
|
| 24 |
|
| 25 |
# ==========================================
|
| 26 |
# 🔑 安全密钥配置
|
|
|
|
| 43 |
|
| 44 |
def hash_password(password: str) -> str:
|
| 45 |
"""
|
| 46 |
+
使用bcrypt哈希密码(内置唯一盐值)
|
| 47 |
|
| 48 |
参数:
|
| 49 |
password: 用户输入的明文密码
|
| 50 |
|
| 51 |
返回:
|
| 52 |
+
bcrypt哈希字符串(以$2b$开头,长度60)
|
| 53 |
|
| 54 |
+
说明:
|
| 55 |
+
- 使用bcrypt算法,自动处理盐值
|
| 56 |
+
- rounds=12提供足够的安全性
|
| 57 |
+
- 每个密码哈希都是唯一的
|
| 58 |
"""
|
| 59 |
+
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12)).decode('utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 63 |
"""
|
| 64 |
+
验证密码,兼容旧版SHA256和新版bcrypt
|
| 65 |
|
| 66 |
参数:
|
| 67 |
plain_password: 用户输入的明文密码
|
|
|
|
| 69 |
|
| 70 |
返回:
|
| 71 |
True 匹配 / False 不匹配
|
| 72 |
+
|
| 73 |
+
兼容逻辑:
|
| 74 |
+
- bcrypt哈希以'$2b$'或'$2a$'开头,长度60
|
| 75 |
+
- 旧版SHA256哈希为64位十六进制字符串
|
| 76 |
"""
|
| 77 |
+
# 检查是否为bcrypt格式
|
| 78 |
+
if hashed_password.startswith('$2b$') or hashed_password.startswith('$2a$'):
|
| 79 |
+
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
| 80 |
+
else:
|
| 81 |
+
# 兼容旧版SHA256哈希(长度64的十六进制字符串)
|
| 82 |
+
salted = f"{PASSWORD_SALT}{plain_password}"
|
| 83 |
+
old_hash = hashlib.sha256(salted.encode("utf-8")).hexdigest()
|
| 84 |
+
return old_hash == hashed_password
|
| 85 |
|
| 86 |
|
| 87 |
def require_password_match(stored_password: str, input_password: str) -> bool:
|
| 88 |
"""
|
| 89 |
+
统一密码验证,支持bcrypt和旧版SHA256
|
| 90 |
|
| 91 |
+
作用:封装密码验证逻辑,兼容新旧密码格式
|
| 92 |
|
| 93 |
参数:
|
| 94 |
stored_password: 数据库中存储的密码(哈希值)
|
|
|
|
| 98 |
True 密码匹配
|
| 99 |
|
| 100 |
异常:
|
| 101 |
+
HTTPException 401: 密码为空或密码错误
|
| 102 |
|
| 103 |
使用场景:
|
| 104 |
- 用户登录时验证密码
|
| 105 |
- 敏感操作前验证当前密码
|
| 106 |
"""
|
| 107 |
+
if not stored_password:
|
| 108 |
+
raise HTTPException(status_code=401, detail="需要重置密码")
|
| 109 |
+
if not verify_password(input_password, stored_password):
|
| 110 |
+
raise HTTPException(status_code=401, detail="密码错误")
|
| 111 |
+
return True
|
|
|
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
# ==========================================
|