From aa82a19ac7bfa0a458b731f1f3d4406c87e8a864 Mon Sep 17 00:00:00 2001
From: fanbook-wangdage <124357765+fanbook-wangdage@users.noreply.github.com>
Date: Sat, 31 Jan 2026 14:25:33 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=AA=8C=E8=AF=81=E7=A0=81?=
=?UTF-8?q?=E7=9A=84=E5=AD=98=E5=82=A8=E6=96=B9=E5=BC=8F=E3=80=81=E6=94=AF?=
=?UTF-8?q?=E6=8C=81html=E9=AA=8C=E8=AF=81=E7=A0=81=E9=82=AE=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 12 ++-
SendEmailTool.py | 10 +-
app/config.py | 4 +
app/config_loader.py | 16 ++++
routes/auth.py | 37 ++++---
services/auth_service.py | 133 +++++++++++++++++++++++---
services/verification_code_service.py | 70 ++++++++++++++
7 files changed, 244 insertions(+), 38 deletions(-)
create mode 100644 services/verification_code_service.py
diff --git a/README.md b/README.md
index 36bc14f..27713e4 100644
--- a/README.md
+++ b/README.md
@@ -48,12 +48,18 @@ print("Keys generated.")
},
"EMAIL": {
"GMAIL_USER": "wdgwdg889@gmail.com",
- "APP_PASSWORD": ""
+ "APP_PASSWORD": "",
+ "APP_NAME": "WDG Snap Hutao",
+ "OFFICIAL_WEBSITE": "https://htserver.wdg.cloudns.ch/",
+ "SUBJECT": "WDG Snap Hutao 验证码"
},
"RSA": {
"PRIVATE_KEY_FILE": "private.pem",
"PUBLIC_KEY_FILE": "public.pem"
},
+ "VERIFICATION_CODE": {
+ "EXPIRE_MINUTES": 10
+ },
"LOGGING": {
"LEVEL": "DEBUG",
"FORMAT": ""
@@ -76,8 +82,12 @@ print("Keys generated.")
| JWT.EXPIRATION_HOURS | JWT过期时间(小时) |
| EMAIL.GMAIL_USER | 用于发送验证邮件的Gmail账号 |
| EMAIL.APP_PASSWORD | Gmail应用专用密码 |
+| EMAIL.APP_NAME | 应用名称,用于邮件显示 |
+| EMAIL.OFFICIAL_WEBSITE | 官方网站地址,用于邮件中的链接 |
+| EMAIL.SUBJECT | 验证邮件的主题 |
| RSA.PRIVATE_KEY_FILE | RSA私钥文件路径 |
| RSA.PUBLIC_KEY_FILE | RSA公钥文件路径 |
+| VERIFICATION_CODE.EXPIRE_MINUTES | 验证码过期时间(分钟) |
| LOGGING.LEVEL | 日志记录级别,生产环境建议设置为INFO |
| LOGGING.FORMAT | 日志记录格式 |
diff --git a/SendEmailTool.py b/SendEmailTool.py
index f33ad2f..c5041ba 100644
--- a/SendEmailTool.py
+++ b/SendEmailTool.py
@@ -9,12 +9,16 @@ app_password = config_loader.EMAIL_APP_PASSWORD
to_email = ""
-def send_email(gmail_user, app_password, to_email, subject, body="这是一封测试邮件。"):
+def send_email(gmail_user, app_password, to_email, subject, body="这是一封测试邮件。", app_name=None, body_type="plain"):
msg = MIMEMultipart()
- msg["From"] = gmail_user
+ # 如果提供了 app_name,使用带显示名称的格式
+ if app_name:
+ msg["From"] = f"{app_name} <{gmail_user}>"
+ else:
+ msg["From"] = gmail_user
msg["To"] = to_email
msg["Subject"] = subject
- msg.attach(MIMEText(body, "plain"))
+ msg.attach(MIMEText(body, body_type))
# Gmail SMTP 服务器
server = smtplib.SMTP("smtp.gmail.com", 587)
diff --git a/app/config.py b/app/config.py
index 56bfd37..dc442b6 100644
--- a/app/config.py
+++ b/app/config.py
@@ -6,3 +6,7 @@ class Config:
MONGO_URI = config_loader.MONGO_URI
TIMEZONE = config_loader.TIMEZONE
ISTEST_MODE = config_loader.ISTEST_MODE
+ EMAIL_APP_NAME = config_loader.EMAIL_APP_NAME
+ EMAIL_OFFICIAL_WEBSITE = config_loader.EMAIL_OFFICIAL_WEBSITE
+ EMAIL_SUBJECT = config_loader.EMAIL_SUBJECT
+ VERIFICATION_CODE_EXPIRE_MINUTES = config_loader.VERIFICATION_CODE_EXPIRE_MINUTES
diff --git a/app/config_loader.py b/app/config_loader.py
index c8801e7..0feb915 100644
--- a/app/config_loader.py
+++ b/app/config_loader.py
@@ -95,6 +95,22 @@ class ConfigLoader:
@property
def LOGGING_FORMAT(self) -> str:
return self.get('LOGGING.FORMAT', '%(asctime)s %(name)s %(levelname)s %(message)s')
+
+ @property
+ def EMAIL_APP_NAME(self) -> str:
+ return self.get('EMAIL.APP_NAME', 'WDG Snap Hutao')
+
+ @property
+ def EMAIL_OFFICIAL_WEBSITE(self) -> str:
+ return self.get('EMAIL.OFFICIAL_WEBSITE', 'https://htserver.wdg.cloudns.ch/')
+
+ @property
+ def EMAIL_SUBJECT(self) -> str:
+ return self.get('EMAIL.SUBJECT', 'WDG Snap Hutao 验证码')
+
+ @property
+ def VERIFICATION_CODE_EXPIRE_MINUTES(self) -> int:
+ return self.get('VERIFICATION_CODE.EXPIRE_MINUTES', 10)
# 创建全局配置实例
config_loader = ConfigLoader()
\ No newline at end of file
diff --git a/routes/auth.py b/routes/auth.py
index a500ba6..51a5bd3 100644
--- a/routes/auth.py
+++ b/routes/auth.py
@@ -1,10 +1,12 @@
-from flask import Blueprint, request, jsonify, session
+from flask import Blueprint, request, jsonify
from app.utils.jwt_utils import create_token, verify_token
from services.auth_service import (
- decrypt_data, send_verification_email, verify_user_credentials,
+ decrypt_data, send_verification_email, verify_user_credentials,
create_user_account, get_user_by_id
)
+from services.verification_code_service import save_verification_code, verify_code
from app.extensions import generate_code, logger , config_loader
+from app.config import Config
auth_bp = Blueprint("auth", __name__)
@@ -14,7 +16,7 @@ def passport_verify():
"""获取验证码"""
data = request.get_json()
encrypted_email = data.get('UserName', '')
-
+
try:
decrypted_email = decrypt_data(encrypted_email)
logger.debug(f"Decrypted email: {decrypted_email}")
@@ -25,12 +27,12 @@ def passport_verify():
"message": f"Invalid encrypted email: {str(e)}",
"data": None
})
-
+
# 生成验证码
code = generate_code(6)
- session['verification_code'] = code
- session['email'] = decrypted_email
-
+ # 使用 MongoDB TTL 存储验证码
+ save_verification_code(decrypted_email, code, expire_minutes=Config.VERIFICATION_CODE_EXPIRE_MINUTES)
+
# 发送邮件
if send_verification_email(decrypted_email, code):
return jsonify({
@@ -53,12 +55,12 @@ def passport_register():
encrypted_email = data.get('UserName', '')
encrypted_password = data.get('Password', '')
encrypted_code = data.get('VerifyCode', '')
-
+
try:
decrypted_email = decrypt_data(encrypted_email)
decrypted_password = decrypt_data(encrypted_password)
decrypted_code = decrypt_data(encrypted_code)
-
+
logger.debug(f"Decrypted registration data: email={decrypted_email}, code={decrypted_code}")
except Exception as e:
logger.warning(f"Decryption error: {e}")
@@ -67,17 +69,16 @@ def passport_register():
"message": f"Invalid encrypted data: {str(e)}",
"data": None
}), 400
-
- # 验证验证码
- if (session.get('verification_code') != decrypted_code or
- session.get('email') != decrypted_email):
+
+ # 使用 MongoDB 验证验证码
+ if not verify_code(decrypted_email, decrypted_code):
logger.warning("Invalid verification code")
return jsonify({
"retcode": 2,
"message": "Invalid verification code",
"data": None
})
-
+
# 创建新用户
new_user = create_user_account(decrypted_email, decrypted_password)
if not new_user:
@@ -87,15 +88,11 @@ def passport_register():
"message": "User already exists",
"data": None
})
-
- # 删除session中的验证码和邮箱
- session.pop('verification_code', None)
- session.pop('email', None)
-
+
# 创建token
access_token = create_token(str(new_user['_id']))
logger.info(f"User registered: {decrypted_email}")
-
+
return jsonify({
"retcode": 0,
"message": "success",
diff --git a/services/auth_service.py b/services/auth_service.py
index 9e9eafc..4ba41b0 100644
--- a/services/auth_service.py
+++ b/services/auth_service.py
@@ -2,9 +2,9 @@ from bson import ObjectId
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import client, logger
from app.config import Config
+from app.config_loader import config_loader
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
-from app.config_loader import config_loader
from datetime import timezone
from zoneinfo import ZoneInfo
import datetime
@@ -26,20 +26,125 @@ def decrypt_data(encrypted_data):
raise
-def send_verification_email(email, code):
- """发送验证码邮件"""
+def send_verification_email(email, code, ACTION_NAME="注册", EXPIRE_MINUTES=None):
+ """发送验证码邮件,目前只有注册场景,后续再扩展其他场景"""
try:
- subject = "Snap Hutao 验证码"
- body = f"您的验证码是: {code}"
-
- SendEmailTool.send_email(
- SendEmailTool.gmail_user,
- SendEmailTool.app_password,
- email,
- subject,
- body
- )
- logger.info(f"Verification email sent to {email}")
+ subject = Config.EMAIL_SUBJECT
+ textbody = f"您的验证码是: {code}"
+ APP_NAME = Config.EMAIL_APP_NAME
+ OFFICIAL_WEBSITE = Config.EMAIL_OFFICIAL_WEBSITE
+ if EXPIRE_MINUTES is None:
+ EXPIRE_MINUTES = Config.VERIFICATION_CODE_EXPIRE_MINUTES
+ htmlbody = f"""
+
+
+
+
+ 验证码邮件
+
+
+
+
+
+
+
+
+
+
+ |
+ {APP_NAME}
+ |
+
+
+
+
+ |
+ 你正在进行 {ACTION_NAME} 操作,请使用以下验证码完成验证:
+ |
+
+
+
+
+ |
+
+ {code}
+
+ |
+
+
+
+
+ |
+ 验证码有效期 {EXPIRE_MINUTES} 分钟,请勿泄露给他人。
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+ 本邮件由 {APP_NAME} 系统自动发送,请勿回复
+ 如非本人操作,请忽略本邮件
+ |
+
+
+
+
+ |
+
+ 访问官网
+
+ |
+
+
+
+ |
+
+
+
+
+
+ """
+ try:
+ SendEmailTool.send_email(
+ config_loader.EMAIL_GMAIL_USER,
+ config_loader.EMAIL_APP_PASSWORD,
+ email,
+ subject,
+ htmlbody,
+ app_name=APP_NAME,
+ body_type="html"
+ )
+ logger.info(f"HTML Verification email sent to {email}")
+ except Exception as e:
+ logger.error(f"Failed to send HTML verification email to {email}: {e}")
+ # 如果 HTML 邮件发送失败,尝试发送纯文本邮件
+ SendEmailTool.send_email(
+ config_loader.EMAIL_GMAIL_USER,
+ config_loader.EMAIL_APP_PASSWORD,
+ email,
+ subject,
+ textbody,
+ app_name=APP_NAME,
+ body_type="plain"
+ )
+ logger.info(f"Verification email sent to {email}")
return True
except Exception as e:
logger.error(f"Failed to send email: {e}")
diff --git a/services/verification_code_service.py b/services/verification_code_service.py
new file mode 100644
index 0000000..561c6b0
--- /dev/null
+++ b/services/verification_code_service.py
@@ -0,0 +1,70 @@
+import datetime
+from app.extensions import client, logger
+
+
+def init_verification_code_collection():
+ """初始化验证码集合并创建 TTL 索引"""
+ db = client.ht_server
+ collection = db.verification_codes
+
+ indexes = collection.list_indexes()
+ ttl_index_exists = False
+ for index in indexes:
+ if index.get('expireAfterSeconds') is not None:
+ ttl_index_exists = True
+ break
+
+ # 如果不存在 TTL 索引,则创建
+ if not ttl_index_exists:
+ collection.create_index(
+ [("expire_at", 1)],
+ expireAfterSeconds=0,
+ name="expire_at_ttl"
+ )
+ logger.info("Created TTL index on verification_codes collection")
+
+
+def save_verification_code(email: str, code: str, expire_minutes: int = 10):
+ """保存验证码到 MongoDB,自动过期时间由 TTL 索引控制"""
+ db = client.ht_server
+ collection = db.verification_codes
+
+ # 确保集合已初始化
+ init_verification_code_collection()
+
+ # 计算过期时间
+ expire_at = datetime.datetime.utcnow() + datetime.timedelta(minutes=expire_minutes)
+
+ # 插入验证码记录
+ result = collection.insert_one({
+ "email": email,
+ "code": code,
+ "created_at": datetime.datetime.utcnow(),
+ "expire_at": expire_at,
+ "used": False
+ })
+
+ logger.debug(f"Saved verification code for email: {email}")
+ return result.inserted_id
+
+
+def verify_code(email: str, code: str) -> bool:
+ """验证验证码是否正确,验证成功后删除验证码记录"""
+ db = client.ht_server
+ collection = db.verification_codes
+
+ # 查找未使用的验证码
+ verification_record = collection.find_one({
+ "email": email,
+ "code": code,
+ "used": False
+ })
+
+ if verification_record:
+ # 验证成功,删除该验证码记录
+ collection.delete_one({"_id": verification_record["_id"]})
+ logger.info(f"Verification code validated and deleted for email: {email}")
+ return True
+
+ logger.warning(f"Invalid or expired verification code for email: {email}")
+ return False
\ No newline at end of file