ZHIWEI666 commited on
Commit
6d79d77
·
verified ·
1 Parent(s): 7b8e13b

Upload 94 files

Browse files
Files changed (5) hide show
  1. app.py +6 -9
  2. models.py +4 -4
  3. router_users_auth.py +51 -56
  4. runtime_log_privacy.py +51 -0
  5. verify_code_engine.py +23 -24
app.py CHANGED
@@ -1,4 +1,7 @@
1
  # ⚙️ 后端逻辑/核心服务端.py (Hugging Face Spaces app.py)
 
 
 
2
  from fastapi import FastAPI, File, UploadFile, Form, Depends, HTTPException, Request
3
  from rate_limit import shared_limit
4
  from response_privacy import requires_no_store
@@ -106,7 +109,7 @@ def create_error_response(status_code: int, detail: str, error_type: str = None,
106
  @app.exception_handler(StarletteHTTPException)
107
  async def http_exception_handler(request, exc):
108
  """🚀 P3优化:HTTP 异常处理,返回统一格式"""
109
- logger.warning(f"HTTP {exc.status_code} | {request.url.path} | detail_type={type(exc.detail).__name__}")
110
 
111
  # 根据状态码确定错误类型
112
  error_type_map = {
@@ -147,13 +150,7 @@ async def private_response_cache_control(request: Request, call_next):
147
  @app.exception_handler(RequestValidationError)
148
  async def validation_exception_handler(request, exc):
149
  """🚀 P3优化:请求参数校验失败处理"""
150
- # Pydantic errors may include rejected input values (passwords, tokens, private text).
151
- # Keep only field paths and machine error codes in public runtime logs.
152
- error_fields = [
153
- {"field": ".".join(str(loc) for loc in err.get("loc", ())[1:]), "type": err.get("type", "unknown")}
154
- for err in exc.errors()
155
- ]
156
- logger.warning(f"Validation Error | {request.url.path} | fields={error_fields}")
157
 
158
  # 提取简洁的错误信息
159
  errors = []
@@ -175,7 +172,7 @@ async def validation_exception_handler(request, exc):
175
  async def global_exception_handler(request, exc):
176
  """🚀 P3优化:全局异常处理,捕获所有未预期异常"""
177
  error_id = f"ERR_{int(time.time())}_{id(exc) % 10000}"
178
- logger.error(f"Unhandled Exception [{error_id}] | {request.url.path} | error_type={type(exc).__name__}")
179
  return JSONResponse(
180
  status_code=500,
181
  content=create_error_response(
 
1
  # ⚙️ 后端逻辑/核心服务端.py (Hugging Face Spaces app.py)
2
+ from runtime_log_privacy import install_uvicorn_privacy_filters, route_label
3
+ install_uvicorn_privacy_filters()
4
+
5
  from fastapi import FastAPI, File, UploadFile, Form, Depends, HTTPException, Request
6
  from rate_limit import shared_limit
7
  from response_privacy import requires_no_store
 
109
  @app.exception_handler(StarletteHTTPException)
110
  async def http_exception_handler(request, exc):
111
  """🚀 P3优化:HTTP 异常处理,返回统一格式"""
112
+ logger.warning(f"HTTP {exc.status_code} | {route_label(request)} | detail_type={type(exc.detail).__name__}")
113
 
114
  # 根据状态码确定错误类型
115
  error_type_map = {
 
150
  @app.exception_handler(RequestValidationError)
151
  async def validation_exception_handler(request, exc):
152
  """🚀 P3优化:请求参数校验失败处理"""
153
+ logger.warning("Validation Error | %s | count=%d", route_label(request), len(exc.errors()))
 
 
 
 
 
 
154
 
155
  # 提取简洁的错误信息
156
  errors = []
 
172
  async def global_exception_handler(request, exc):
173
  """🚀 P3优化:全局异常处理,捕获所有未预期异常"""
174
  error_id = f"ERR_{int(time.time())}_{id(exc) % 10000}"
175
+ logger.error(f"Unhandled Exception [{error_id}] | {route_label(request)} | error_type={type(exc).__name__}")
176
  return JSONResponse(
177
  status_code=500,
178
  content=create_error_response(
models.py CHANGED
@@ -1,7 +1,7 @@
1
  # models.py
2
  import re
3
  from pydantic import BaseModel, Field, field_validator # 🧹 P2修复:pydantic v2 已废弃 v1 风格 validator
4
- from typing import Optional, List
5
 
6
  def _validate_email_format(email: str) -> str:
7
  """邮箱格式校验"""
@@ -18,9 +18,9 @@ def _validate_password_length(password: str) -> str:
18
  return password
19
 
20
  class SendCodeRequest(BaseModel):
21
- contact: str
22
- contact_type: str
23
- action_type: str
24
  account: Optional[str] = None
25
 
26
  class UserRegister(BaseModel):
 
1
  # models.py
2
  import re
3
  from pydantic import BaseModel, Field, field_validator # 🧹 P2修复:pydantic v2 已废弃 v1 风格 validator
4
+ from typing import Optional, List, Literal
5
 
6
  def _validate_email_format(email: str) -> str:
7
  """邮箱格式校验"""
 
18
  return password
19
 
20
  class SendCodeRequest(BaseModel):
21
+ contact: str = Field(..., min_length=1, max_length=254)
22
+ contact_type: Literal["email", "phone"]
23
+ action_type: Literal["register", "reset", "recover", "withdraw"]
24
  account: Optional[str] = None
25
 
26
  class UserRegister(BaseModel):
router_users_auth.py CHANGED
@@ -15,7 +15,7 @@ from rate_limit import shared_limit, store_verification_code, consume_verificati
15
  import asyncio
16
  import time
17
  import re
18
- import random
19
  import json
20
  import logging
21
  import 数据库连接 as db
@@ -36,6 +36,35 @@ logger = logging.getLogger("ComfyUI-Ranking.UsersAuth")
36
  router = APIRouter()
37
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  # ==========================================
40
  # 📤 发送验证码接口(异步后台任务版)
41
  # ==========================================
@@ -54,38 +83,14 @@ async def send_verify_code(request: Request, req: SendCodeRequest, bg_tasks: Bac
54
  - action_type: "register" 注册 / "reset" 重置密码 / "recover" 找回账号
55
  - account: (仅重置密码时必填) 用户账号
56
  """
57
- # 🔎 找回账号:凭绑定邮箱/手机反查,无需账号
58
- if req.action_type == "recover":
59
- users_db = db.load_data("users.json", default_data={})
60
- matched = any(
61
- u.get("email") == req.contact or u.get("phone") == req.contact
62
- for u in users_db.values()
63
- )
64
- if not matched:
65
- # 🛡️ 防枚举:未注册的联系方式也返回通用成功,但不实际发送验证码
66
- return {"status": "success", "message": "如果该联系方式已绑定账号,验证码已发送"}
67
-
68
- # 如果是重置密码,需要先验证账号与联系方式匹配
69
- if req.action_type == "reset":
70
- if not req.account:
71
- raise HTTPException(status_code=400, detail="找回密码需先填写当前账号")
72
-
73
- users_db = db.load_data("users.json", default_data={})
74
- user = users_db.get(req.account)
75
- if not user:
76
- raise HTTPException(status_code=404, detail="该账号不存在")
77
-
78
- # 校验联系方式是否与账号绑定的一致
79
- if req.contact_type == "email" and user.get("email") != req.contact:
80
- raise HTTPException(status_code=400, detail="填写的邮箱与该账号绑定的邮箱不一致")
81
- if req.contact_type == "phone" and user.get("phone") != req.contact:
82
- raise HTTPException(status_code=400, detail="填写的手机号与该账号绑定的手机号不一致")
83
 
84
  # 🔒 检查发送频率限制(同一联系方式60秒内只能发送1条)
85
  check_send_cooldown(req.contact)
86
 
87
  # 生成6位随机验证码
88
- code = str(random.randint(100000, 999999))
89
 
90
  # 🚀 P1性能优化:每次写入前触发清理过期验证码
91
  cleanup_expired_codes()
@@ -116,32 +121,20 @@ async def send_verify_code(request: Request, req: SendCodeRequest, bg_tasks: Bac
116
  @shared_limit(5) # 🔒 安全限流:与异步版对齐,每分钟最多5次
117
  async def send_code_api(request: Request, req: SendCodeRequest):
118
  """发送验证码接口(同步版本,直接等待发送结果)"""
119
- # 🔎 找回账号:凭绑定邮箱/手机反查,无需账号
120
- if req.action_type == "recover":
121
- users_db = db.load_data("users.json", default_data={})
122
- matched = any(
123
- u.get("email") == req.contact or u.get("phone") == req.contact
124
- for u in users_db.values()
125
- )
126
- if not matched:
127
- # 🛡️ 防枚举:未注册的联系方式也返回通用成功,但不实际发送验证码
128
- return {"status": "success", "message": "如果该联系方式已绑定账号,验证码已发送"}
129
 
130
  # 🔒 检查发送频率限制(同一联系方式60秒内只能发送1条)
131
  check_send_cooldown(req.contact)
132
 
133
  # 生成6位随机验证码
134
- code = str(random.randint(100000, 999999))
135
  # 🔒 P0安全加固:使用冒号分隔符防止跨操作重用验证码
136
  key = f"{req.contact}:{req.action_type}"
137
 
138
  # 存入缓存,有效期5分钟
139
  await asyncio.to_thread(store_verification_code, key, code, int(time.time()) + 300)
140
 
141
- # 同步发送(会阻塞等待结果)
142
- # ⚡ 性能修复:send_email_code 内部走 SMTP 同步连接(最长可阻塞十秒级),
143
- # 直接在 async 处理器里调用会占死唯一的事件循环,导致整个服务期间无法响应任何请求;
144
- # 改由 asyncio.to_thread 丢到线程池执行,等待结果的同时不阻塞其他请求。
145
  if req.contact_type == "email":
146
  try:
147
  await asyncio.to_thread(send_email_code, req.contact, code, req.action_type)
@@ -149,10 +142,15 @@ async def send_code_api(request: Request, req: SendCodeRequest):
149
  except Exception as e:
150
  # 🔒 P0安全修复:仅服务端记录详细错误,不暴露给前端
151
  logger.error(f"验证码邮件发送失败: action={req.action_type}, error_type={type(e).__name__}")
152
- raise HTTPException(status_code=500, detail="邮件发送失败,请稍后重试")
153
 
154
  elif req.contact_type == "phone":
155
- return {"status": "success", "message": "验证码已成功发送至手机"}
 
 
 
 
 
156
 
157
  else:
158
  raise HTTPException(status_code=400, detail="不支持的验证方式")
@@ -187,10 +185,12 @@ async def recover_account(request: Request):
187
  if not isinstance(data, dict):
188
  raise HTTPException(status_code=400, detail="请求数据格式异常")
189
 
190
- email = (data.get("email") or data.get("contact") or "").strip()
191
- code = (data.get("code") or "").strip()
192
-
193
- if not email or not code:
 
 
194
  raise HTTPException(status_code=400, detail="缺失必要参数 (邮箱/验证码)")
195
 
196
  # ========== 第二步:验证码校验(原子性一次性消费,并发安全) ==========
@@ -200,12 +200,7 @@ async def recover_account(request: Request):
200
  raise HTTPException(status_code=400, detail="验证码不正确或已过期")
201
 
202
  # ========== 第三步:按邮箱反查账号 ==========
203
- users_db = db.load_data("users.json", default_data={})
204
- matched_account = None
205
- for account, user in users_db.items():
206
- if user.get("email") == email:
207
- matched_account = account
208
- break
209
 
210
  if not matched_account:
211
  # 理论上验证码已验证邮箱存在,此处为防御性分支
@@ -215,7 +210,7 @@ async def recover_account(request: Request):
215
  "status": "success",
216
  "message": "账号找回成功",
217
  "account": matched_account,
218
- "name": users_db[matched_account].get("name", "")
219
  }
220
 
221
 
 
15
  import asyncio
16
  import time
17
  import re
18
+ import secrets
19
  import json
20
  import logging
21
  import 数据库连接 as db
 
36
  router = APIRouter()
37
 
38
 
39
+ def _find_contact_account(contact, contact_type):
40
+ try:
41
+ users = db.load_data("users.json", default_data={}, strict=True)
42
+ if not isinstance(users, dict):
43
+ raise ValueError("invalid_users")
44
+ for account, user in users.items():
45
+ if not isinstance(account, str) or not isinstance(user, dict):
46
+ raise ValueError("invalid_user")
47
+ if user.get(contact_type) == contact:
48
+ return account, user
49
+ return None, None
50
+ except Exception as error:
51
+ logger.warning("账号联系方式读取失败: error_type=%s", type(error).__name__)
52
+ raise HTTPException(status_code=503, detail="用户数据暂不可用") from None
53
+
54
+
55
+ async def _check_code_contact(req):
56
+ if req.action_type == "recover":
57
+ account, _ = await asyncio.to_thread(_find_contact_account, req.contact, req.contact_type)
58
+ return account is not None
59
+ if req.action_type == "reset":
60
+ if not req.account:
61
+ raise HTTPException(status_code=400, detail="找回密码需先填写当前账号")
62
+ user = await asyncio.to_thread(read_user, req.account)
63
+ if user.get(req.contact_type) != req.contact:
64
+ raise HTTPException(status_code=400, detail="填写的联系方式与该账号绑定信息不匹配")
65
+ return True
66
+
67
+
68
  # ==========================================
69
  # 📤 发送验证码接口(异步后台任务版)
70
  # ==========================================
 
83
  - action_type: "register" 注册 / "reset" 重置密码 / "recover" 找回账号
84
  - account: (仅重置密码时必填) 用户账号
85
  """
86
+ if not await _check_code_contact(req):
87
+ return {"status": "success", "message": "如果该联系方式已绑定账号,验证码已发送"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  # 🔒 检查发送频率限制(同一联系方式60秒内只能发送1条)
90
  check_send_cooldown(req.contact)
91
 
92
  # 生成6位随机验证码
93
+ code = str(secrets.randbelow(900000) + 100000)
94
 
95
  # 🚀 P1性能优化:每次写入前触发清理过期验证码
96
  cleanup_expired_codes()
 
121
  @shared_limit(5) # 🔒 安全限流:与异步版对齐,每分钟最多5次
122
  async def send_code_api(request: Request, req: SendCodeRequest):
123
  """发送验证码接口(同步版本,直接等待发送结果)"""
124
+ if not await _check_code_contact(req):
125
+ return {"status": "success", "message": "如果该联系方式已绑定账号,验证码已发送"}
 
 
 
 
 
 
 
 
126
 
127
  # 🔒 检查发送频率限制(同一联系方式60秒内只能发送1条)
128
  check_send_cooldown(req.contact)
129
 
130
  # 生成6位随机验证码
131
+ code = str(secrets.randbelow(900000) + 100000)
132
  # 🔒 P0安全加固:使用冒号分隔符防止跨操作重用验证码
133
  key = f"{req.contact}:{req.action_type}"
134
 
135
  # 存入缓存,有效期5分钟
136
  await asyncio.to_thread(store_verification_code, key, code, int(time.time()) + 300)
137
 
 
 
 
 
138
  if req.contact_type == "email":
139
  try:
140
  await asyncio.to_thread(send_email_code, req.contact, code, req.action_type)
 
142
  except Exception as e:
143
  # 🔒 P0安全修复:仅服务端记录详细错误,不暴露给前端
144
  logger.error(f"验证码邮件发送失败: action={req.action_type}, error_type={type(e).__name__}")
145
+ raise HTTPException(status_code=503, detail="邮件发送失败,请稍后重试") from None
146
 
147
  elif req.contact_type == "phone":
148
+ try:
149
+ await asyncio.to_thread(send_sms_code, req.contact, code, req.action_type)
150
+ return {"status": "success", "message": "验证码已成功发送至手机"}
151
+ except Exception as error:
152
+ logger.error("验证码短信发送失败: action=%s, error_type=%s", req.action_type, type(error).__name__)
153
+ raise HTTPException(status_code=503, detail="短信发送失败,请稍后重试") from None
154
 
155
  else:
156
  raise HTTPException(status_code=400, detail="不支持的验证方式")
 
185
  if not isinstance(data, dict):
186
  raise HTTPException(status_code=400, detail="请求数据格式异常")
187
 
188
+ email = data.get("email") or data.get("contact") or ""
189
+ code = data.get("code") or ""
190
+ if not isinstance(email, str) or not isinstance(code, str):
191
+ raise HTTPException(status_code=400, detail="请求参数格式不正确")
192
+ email, code = email.strip(), code.strip()
193
+ if not email or len(email) > 254 or not re.fullmatch(r"[0-9]{6}", code):
194
  raise HTTPException(status_code=400, detail="缺失必要参数 (邮箱/验证码)")
195
 
196
  # ========== 第二步:验证码校验(原子性一次性消费,并发安全) ==========
 
200
  raise HTTPException(status_code=400, detail="验证码不正确或已过期")
201
 
202
  # ========== 第三步:按邮箱反查账号 ==========
203
+ matched_account, matched_user = await asyncio.to_thread(_find_contact_account, email, "email")
 
 
 
 
 
204
 
205
  if not matched_account:
206
  # 理论上验证码已验证邮箱存在,此处为防御性分支
 
210
  "status": "success",
211
  "message": "账号找回成功",
212
  "account": matched_account,
213
+ "name": matched_user.get("name", "")
214
  }
215
 
216
 
runtime_log_privacy.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """运行服务器日志不记录请求目标、客户端标识或异常正文。"""
2
+ import logging
3
+
4
+
5
+ def route_label(request):
6
+ route = request.scope.get('route')
7
+ label = getattr(route, 'path', None)
8
+ if (not isinstance(label, str) or len(label) > 256 or not label.startswith('/')
9
+ or any(char not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_{}.-:' for char in label)):
10
+ return '<unmatched>'
11
+ return label
12
+
13
+
14
+ class UvicornPrivacyFilter(logging.Filter):
15
+ def filter(self, record):
16
+ if record.name == 'uvicorn.access':
17
+ args = record.args
18
+ method = version = 'unknown'
19
+ status = 0
20
+ if isinstance(args, tuple) and len(args) == 5:
21
+ if type(args[1]) is str and args[1] in {'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'CONNECT', 'TRACE'}:
22
+ method = args[1]
23
+ if type(args[3]) is str and args[3] in {'1.0', '1.1', '2', '2.0', '3'}:
24
+ version = args[3]
25
+ if type(args[4]) is int and 100 <= args[4] <= 599:
26
+ status = args[4]
27
+ record.msg = '%s - "%s %s HTTP/%s" %d'
28
+ record.args = ('[client]', method, '[request]', version, status)
29
+ record.exc_info = record.exc_text = record.stack_info = None
30
+ elif record.exc_info or record.exc_text or record.stack_info or record.levelno >= logging.ERROR:
31
+ error = record.exc_info[1] if isinstance(record.exc_info, tuple) and len(record.exc_info) == 3 else None
32
+ kind = type(error).__name__ if error is not None else 'unknown'
33
+ if not kind.isidentifier() or len(kind) > 80:
34
+ kind = 'unknown'
35
+ record.msg = 'SERVER_ERROR | error_type=%s'
36
+ record.args = (kind,)
37
+ record.exc_info = record.exc_text = record.stack_info = None
38
+ elif record.name == 'uvicorn.error' and type(record.msg) is str and record.msg in {
39
+ '%s - "WebSocket %s" [accepted]', '%s - "WebSocket %s" 403'
40
+ }:
41
+ record.args = ('[client]', '[request]')
42
+ elif record.name == 'uvicorn.error' and record.levelno < logging.DEBUG:
43
+ return False
44
+ return True
45
+
46
+
47
+ def install_uvicorn_privacy_filters():
48
+ for name in ('uvicorn.access', 'uvicorn.error'):
49
+ logger = logging.getLogger(name)
50
+ if not any(isinstance(item, UvicornPrivacyFilter) for item in logger.filters):
51
+ logger.addFilter(UvicornPrivacyFilter())
verify_code_engine.py CHANGED
@@ -30,8 +30,6 @@ _CLEANUP_INTERVAL = 300 # 5分钟清理一次
30
  # 🔒 日志脱敏工具
31
  # ==========================================
32
  # 作用:HF Spaces 的运行日志对外可见,验证码与联系方式明文入日志等同于泄露账号
33
- # 关联:云端/本地判据与 数据库连接.py 保持一致(HF Spaces 会注入 SPACE_ID)
34
- _IS_CLOUD = bool(os.environ.get("SPACE_ID"))
35
 
36
 
37
  def _mask_contact(contact: str) -> str:
@@ -67,12 +65,12 @@ def check_send_cooldown(contact: str):
67
 
68
 
69
  def cleanup_expired_codes():
70
- """验证码已存入 SQL;清理由限流模块统一按时间间隔执行。"""
71
- global _last_cleanup_time
72
- now = time.time()
73
- if now - _last_cleanup_time < _CLEANUP_INTERVAL:
74
- return
75
- _last_cleanup_time = now
76
 
77
  def send_email_code(to_email: str, code: str, action: str):
78
  """
@@ -91,8 +89,7 @@ def send_email_code(to_email: str, code: str, action: str):
91
  webhook_url = os.environ.get("MAKE_WEBHOOK_URL")
92
 
93
  if not webhook_url:
94
- print("警告: 未配置 MAKE_WEBHOOK_URL,跳过邮件发送")
95
- return
96
 
97
  # 根据动作类型生成不同的邮件标题
98
  if action == "register":
@@ -133,6 +130,8 @@ def send_email_code(to_email: str, code: str, action: str):
133
  )
134
  try:
135
  with urllib.request.urlopen(req, timeout=10) as response:
 
 
136
  # 🔒 安全修复:不再打印验证码与邮箱明文(日志对外可见),仅留脱敏标识与动作类型
137
  print(f"✅ 成功触发 Webhook 发送验证码邮件至 {_mask_contact(to_email)}(动作: {action})")
138
  except Exception as e:
@@ -141,6 +140,7 @@ def send_email_code(to_email: str, code: str, action: str):
141
  status = getattr(e, "code", None)
142
  status_text = f", status={status}" if isinstance(status, int) and 100 <= status <= 599 else ""
143
  print(f"❌ Webhook 触发失败: action={action}, target={_mask_contact(to_email)}, error_type={type(e).__name__}{status_text}")
 
144
 
145
 
146
  def send_sms_code(phone: str, code: str, action: str):
@@ -175,6 +175,8 @@ def send_sms_code(phone: str, code: str, action: str):
175
  if twilio_sid:
176
  token = os.environ.get("TWILIO_TOKEN")
177
  from_phone = os.environ.get("TWILIO_FROM")
 
 
178
 
179
  # 构建短信内容
180
  body = f"【ComfyUI社区】您正在请求{action_str},验证码是:{code},5分钟内有效。"
@@ -188,6 +190,8 @@ def send_sms_code(phone: str, code: str, action: str):
188
  req.add_header("Authorization", f"Basic {auth}")
189
  try:
190
  with urllib.request.urlopen(req, timeout=10) as response:
 
 
191
  print(f"✅ Twilio 短信已成功下发至 {_mask_contact(phone)}")
192
  except Exception as e:
193
  status = getattr(getattr(e, "response", None), "status_code", None)
@@ -195,6 +199,7 @@ def send_sms_code(phone: str, code: str, action: str):
195
  status = getattr(e, "code", None)
196
  status_text = f", status={status}" if isinstance(status, int) and 100 <= status <= 599 else ""
197
  print(f"❌ Twilio 发送失败: target={_mask_contact(phone)}, error_type={type(e).__name__}{status_text}")
 
198
  return
199
 
200
  # ==========================================
@@ -210,14 +215,16 @@ def send_sms_code(phone: str, code: str, action: str):
210
  from alibabacloud_tea_openapi import models as open_api_models
211
  from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
212
  except ImportError:
213
- print("❌ 缺少阿里云 SDK,请在 requirements.txt 中添加 alibabacloud_dysmsapi20170525")
214
- return
215
 
216
  sk = os.environ.get("ALIYUN_SK")
217
  sign_name = os.environ.get("ALIYUN_SIGN_NAME") # 短信签名,如 "阿里云"
218
  # 根据动作类型选择不同的模板
219
  tpl_code = os.environ.get("ALIYUN_TPL_REGISTER") if action == "register" else os.environ.get("ALIYUN_TPL_RESET")
220
 
 
 
 
221
  # 初始化阿里云客户端
222
  config = open_api_models.Config(access_key_id=aliyun_ak, access_key_secret=sk)
223
  config.endpoint = 'dysmsapi.aliyuncs.com'
@@ -235,19 +242,11 @@ def send_sms_code(phone: str, code: str, action: str):
235
  if response.body.code == "OK":
236
  print(f"✅ 阿里云短信已成功下发至 {_mask_contact(phone)}")
237
  else:
238
- response_code = getattr(response.body, "code", "unknown")
239
- print(f"❌ 阿里云下发失败: target={_mask_contact(phone)}, provider_code={response_code}")
240
  except Exception as e:
241
  print(f"❌ 阿里云请求异常: target={_mask_contact(phone)}, error_type={type(e).__name__}")
 
242
  return
243
 
244
- # ==========================================
245
- # 降级模式:控制台打印模拟
246
- # ==========================================
247
- # 作用:当没有配置任何短信服务时,仅在控制台打印验证码(仅限开发测试)
248
- # 🔒 P0安全修复:云端日志对外可见,生产环境绝不能打印验证码明文;
249
- # 本地开发(无 SPACE_ID)保留原行为,不影响调试便利性。
250
- if _IS_CLOUD:
251
- print(f"⚠️ 未配置短信密钥,验证码未实际下发(目标: {_mask_contact(phone)})")
252
- else:
253
- print(f"⚠️ 未配置短信密钥,模拟下发 -> 手机号: {phone}, 验证码: {code}")
 
30
  # 🔒 日志脱敏工具
31
  # ==========================================
32
  # 作用:HF Spaces 的运行日志对外可见,验证码与联系方式明文入日志等同于泄露账号
 
 
33
 
34
 
35
  def _mask_contact(contact: str) -> str:
 
65
 
66
 
67
  def cleanup_expired_codes():
68
+ """验证码已存入 SQL;清理由限流模块统一按时间间隔执行。"""
69
+ global _last_cleanup_time
70
+ now = time.time()
71
+ if now - _last_cleanup_time < _CLEANUP_INTERVAL:
72
+ return
73
+ _last_cleanup_time = now
74
 
75
  def send_email_code(to_email: str, code: str, action: str):
76
  """
 
89
  webhook_url = os.environ.get("MAKE_WEBHOOK_URL")
90
 
91
  if not webhook_url:
92
+ raise RuntimeError("verification_delivery_unavailable")
 
93
 
94
  # 根据动作类型生成不同的邮件标题
95
  if action == "register":
 
130
  )
131
  try:
132
  with urllib.request.urlopen(req, timeout=10) as response:
133
+ if not 200 <= response.status < 300:
134
+ raise RuntimeError("verification_delivery_unconfirmed")
135
  # 🔒 安全修复:不再打印验证码与邮箱明文(日志对外可见),仅留脱敏标识与动作类型
136
  print(f"✅ 成功触发 Webhook 发送验证码邮件至 {_mask_contact(to_email)}(动作: {action})")
137
  except Exception as e:
 
140
  status = getattr(e, "code", None)
141
  status_text = f", status={status}" if isinstance(status, int) and 100 <= status <= 599 else ""
142
  print(f"❌ Webhook 触发失败: action={action}, target={_mask_contact(to_email)}, error_type={type(e).__name__}{status_text}")
143
+ raise RuntimeError("verification_delivery_unconfirmed") from None
144
 
145
 
146
  def send_sms_code(phone: str, code: str, action: str):
 
175
  if twilio_sid:
176
  token = os.environ.get("TWILIO_TOKEN")
177
  from_phone = os.environ.get("TWILIO_FROM")
178
+ if not token or not from_phone:
179
+ raise RuntimeError("verification_delivery_unavailable")
180
 
181
  # 构建短信内容
182
  body = f"【ComfyUI社区】您正在请求{action_str},验证码是:{code},5分钟内有效。"
 
190
  req.add_header("Authorization", f"Basic {auth}")
191
  try:
192
  with urllib.request.urlopen(req, timeout=10) as response:
193
+ if not 200 <= response.status < 300:
194
+ raise RuntimeError("verification_delivery_unconfirmed")
195
  print(f"✅ Twilio 短信已成功下发至 {_mask_contact(phone)}")
196
  except Exception as e:
197
  status = getattr(getattr(e, "response", None), "status_code", None)
 
199
  status = getattr(e, "code", None)
200
  status_text = f", status={status}" if isinstance(status, int) and 100 <= status <= 599 else ""
201
  print(f"❌ Twilio 发送失败: target={_mask_contact(phone)}, error_type={type(e).__name__}{status_text}")
202
+ raise RuntimeError("verification_delivery_unconfirmed") from None
203
  return
204
 
205
  # ==========================================
 
215
  from alibabacloud_tea_openapi import models as open_api_models
216
  from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
217
  except ImportError:
218
+ raise RuntimeError("verification_delivery_unavailable") from None
 
219
 
220
  sk = os.environ.get("ALIYUN_SK")
221
  sign_name = os.environ.get("ALIYUN_SIGN_NAME") # 短信签名,如 "阿里云"
222
  # 根据动作类型选择不同的模板
223
  tpl_code = os.environ.get("ALIYUN_TPL_REGISTER") if action == "register" else os.environ.get("ALIYUN_TPL_RESET")
224
 
225
+ if not sk or not sign_name or not tpl_code:
226
+ raise RuntimeError("verification_delivery_unavailable")
227
+
228
  # 初始化阿里云客户端
229
  config = open_api_models.Config(access_key_id=aliyun_ak, access_key_secret=sk)
230
  config.endpoint = 'dysmsapi.aliyuncs.com'
 
242
  if response.body.code == "OK":
243
  print(f"✅ 阿里云短信已成功下发至 {_mask_contact(phone)}")
244
  else:
245
+ print(f"❌ 阿里云下发失败: target={_mask_contact(phone)}")
246
+ raise RuntimeError("verification_delivery_unconfirmed")
247
  except Exception as e:
248
  print(f"❌ 阿里云请求异常: target={_mask_contact(phone)}, error_type={type(e).__name__}")
249
+ raise RuntimeError("verification_delivery_unconfirmed") from None
250
  return
251
 
252
+ raise RuntimeError("verification_delivery_unavailable")