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