mirror of
https://github.com/wangdage12/Snap.Server.git
synced 2026-02-17 08:52:10 +08:00
12
README.md
12
README.md
@@ -48,12 +48,18 @@ print("Keys generated.")
|
|||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"GMAIL_USER": "wdgwdg889@gmail.com",
|
"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": {
|
"RSA": {
|
||||||
"PRIVATE_KEY_FILE": "private.pem",
|
"PRIVATE_KEY_FILE": "private.pem",
|
||||||
"PUBLIC_KEY_FILE": "public.pem"
|
"PUBLIC_KEY_FILE": "public.pem"
|
||||||
},
|
},
|
||||||
|
"VERIFICATION_CODE": {
|
||||||
|
"EXPIRE_MINUTES": 10
|
||||||
|
},
|
||||||
"LOGGING": {
|
"LOGGING": {
|
||||||
"LEVEL": "DEBUG",
|
"LEVEL": "DEBUG",
|
||||||
"FORMAT": ""
|
"FORMAT": ""
|
||||||
@@ -76,8 +82,12 @@ print("Keys generated.")
|
|||||||
| JWT.EXPIRATION_HOURS | JWT过期时间(小时) |
|
| JWT.EXPIRATION_HOURS | JWT过期时间(小时) |
|
||||||
| EMAIL.GMAIL_USER | 用于发送验证邮件的Gmail账号 |
|
| EMAIL.GMAIL_USER | 用于发送验证邮件的Gmail账号 |
|
||||||
| EMAIL.APP_PASSWORD | Gmail应用专用密码 |
|
| EMAIL.APP_PASSWORD | Gmail应用专用密码 |
|
||||||
|
| EMAIL.APP_NAME | 应用名称,用于邮件显示 |
|
||||||
|
| EMAIL.OFFICIAL_WEBSITE | 官方网站地址,用于邮件中的链接 |
|
||||||
|
| EMAIL.SUBJECT | 验证邮件的主题 |
|
||||||
| RSA.PRIVATE_KEY_FILE | RSA私钥文件路径 |
|
| RSA.PRIVATE_KEY_FILE | RSA私钥文件路径 |
|
||||||
| RSA.PUBLIC_KEY_FILE | RSA公钥文件路径 |
|
| RSA.PUBLIC_KEY_FILE | RSA公钥文件路径 |
|
||||||
|
| VERIFICATION_CODE.EXPIRE_MINUTES | 验证码过期时间(分钟) |
|
||||||
| LOGGING.LEVEL | 日志记录级别,生产环境建议设置为INFO |
|
| LOGGING.LEVEL | 日志记录级别,生产环境建议设置为INFO |
|
||||||
| LOGGING.FORMAT | 日志记录格式 |
|
| LOGGING.FORMAT | 日志记录格式 |
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ app_password = config_loader.EMAIL_APP_PASSWORD
|
|||||||
|
|
||||||
to_email = ""
|
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 = 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["To"] = to_email
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg.attach(MIMEText(body, "plain"))
|
msg.attach(MIMEText(body, body_type))
|
||||||
|
|
||||||
# Gmail SMTP 服务器
|
# Gmail SMTP 服务器
|
||||||
server = smtplib.SMTP("smtp.gmail.com", 587)
|
server = smtplib.SMTP("smtp.gmail.com", 587)
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ class Config:
|
|||||||
MONGO_URI = config_loader.MONGO_URI
|
MONGO_URI = config_loader.MONGO_URI
|
||||||
TIMEZONE = config_loader.TIMEZONE
|
TIMEZONE = config_loader.TIMEZONE
|
||||||
ISTEST_MODE = config_loader.ISTEST_MODE
|
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
|
||||||
|
|||||||
@@ -95,6 +95,22 @@ class ConfigLoader:
|
|||||||
@property
|
@property
|
||||||
def LOGGING_FORMAT(self) -> str:
|
def LOGGING_FORMAT(self) -> str:
|
||||||
return self.get('LOGGING.FORMAT', '%(asctime)s %(name)s %(levelname)s %(message)s')
|
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()
|
config_loader = ConfigLoader()
|
||||||
@@ -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 app.utils.jwt_utils import create_token, verify_token
|
||||||
from services.auth_service import (
|
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
|
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.extensions import generate_code, logger , config_loader
|
||||||
|
from app.config import Config
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__)
|
auth_bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ def passport_verify():
|
|||||||
"""获取验证码"""
|
"""获取验证码"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
encrypted_email = data.get('UserName', '')
|
encrypted_email = data.get('UserName', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decrypted_email = decrypt_data(encrypted_email)
|
decrypted_email = decrypt_data(encrypted_email)
|
||||||
logger.debug(f"Decrypted email: {decrypted_email}")
|
logger.debug(f"Decrypted email: {decrypted_email}")
|
||||||
@@ -25,12 +27,12 @@ def passport_verify():
|
|||||||
"message": f"Invalid encrypted email: {str(e)}",
|
"message": f"Invalid encrypted email: {str(e)}",
|
||||||
"data": None
|
"data": None
|
||||||
})
|
})
|
||||||
|
|
||||||
# 生成验证码
|
# 生成验证码
|
||||||
code = generate_code(6)
|
code = generate_code(6)
|
||||||
session['verification_code'] = code
|
# 使用 MongoDB TTL 存储验证码
|
||||||
session['email'] = decrypted_email
|
save_verification_code(decrypted_email, code, expire_minutes=Config.VERIFICATION_CODE_EXPIRE_MINUTES)
|
||||||
|
|
||||||
# 发送邮件
|
# 发送邮件
|
||||||
if send_verification_email(decrypted_email, code):
|
if send_verification_email(decrypted_email, code):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -53,12 +55,12 @@ def passport_register():
|
|||||||
encrypted_email = data.get('UserName', '')
|
encrypted_email = data.get('UserName', '')
|
||||||
encrypted_password = data.get('Password', '')
|
encrypted_password = data.get('Password', '')
|
||||||
encrypted_code = data.get('VerifyCode', '')
|
encrypted_code = data.get('VerifyCode', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decrypted_email = decrypt_data(encrypted_email)
|
decrypted_email = decrypt_data(encrypted_email)
|
||||||
decrypted_password = decrypt_data(encrypted_password)
|
decrypted_password = decrypt_data(encrypted_password)
|
||||||
decrypted_code = decrypt_data(encrypted_code)
|
decrypted_code = decrypt_data(encrypted_code)
|
||||||
|
|
||||||
logger.debug(f"Decrypted registration data: email={decrypted_email}, code={decrypted_code}")
|
logger.debug(f"Decrypted registration data: email={decrypted_email}, code={decrypted_code}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Decryption error: {e}")
|
logger.warning(f"Decryption error: {e}")
|
||||||
@@ -67,17 +69,16 @@ def passport_register():
|
|||||||
"message": f"Invalid encrypted data: {str(e)}",
|
"message": f"Invalid encrypted data: {str(e)}",
|
||||||
"data": None
|
"data": None
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# 验证验证码
|
# 使用 MongoDB 验证验证码
|
||||||
if (session.get('verification_code') != decrypted_code or
|
if not verify_code(decrypted_email, decrypted_code):
|
||||||
session.get('email') != decrypted_email):
|
|
||||||
logger.warning("Invalid verification code")
|
logger.warning("Invalid verification code")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"retcode": 2,
|
"retcode": 2,
|
||||||
"message": "Invalid verification code",
|
"message": "Invalid verification code",
|
||||||
"data": None
|
"data": None
|
||||||
})
|
})
|
||||||
|
|
||||||
# 创建新用户
|
# 创建新用户
|
||||||
new_user = create_user_account(decrypted_email, decrypted_password)
|
new_user = create_user_account(decrypted_email, decrypted_password)
|
||||||
if not new_user:
|
if not new_user:
|
||||||
@@ -87,15 +88,11 @@ def passport_register():
|
|||||||
"message": "User already exists",
|
"message": "User already exists",
|
||||||
"data": None
|
"data": None
|
||||||
})
|
})
|
||||||
|
|
||||||
# 删除session中的验证码和邮箱
|
|
||||||
session.pop('verification_code', None)
|
|
||||||
session.pop('email', None)
|
|
||||||
|
|
||||||
# 创建token
|
# 创建token
|
||||||
access_token = create_token(str(new_user['_id']))
|
access_token = create_token(str(new_user['_id']))
|
||||||
logger.info(f"User registered: {decrypted_email}")
|
logger.info(f"User registered: {decrypted_email}")
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"retcode": 0,
|
"retcode": 0,
|
||||||
"message": "success",
|
"message": "success",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ from bson import ObjectId
|
|||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from app.extensions import client, logger
|
from app.extensions import client, logger
|
||||||
from app.config import Config
|
from app.config import Config
|
||||||
|
from app.config_loader import config_loader
|
||||||
from Crypto.Cipher import PKCS1_OAEP
|
from Crypto.Cipher import PKCS1_OAEP
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from app.config_loader import config_loader
|
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
import datetime
|
import datetime
|
||||||
@@ -26,20 +26,125 @@ def decrypt_data(encrypted_data):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def send_verification_email(email, code):
|
def send_verification_email(email, code, ACTION_NAME="注册", EXPIRE_MINUTES=None):
|
||||||
"""发送验证码邮件"""
|
"""发送验证码邮件,目前只有注册场景,后续再扩展其他场景"""
|
||||||
try:
|
try:
|
||||||
subject = "Snap Hutao 验证码"
|
subject = Config.EMAIL_SUBJECT
|
||||||
body = f"您的验证码是: {code}"
|
textbody = f"您的验证码是: {code}"
|
||||||
|
APP_NAME = Config.EMAIL_APP_NAME
|
||||||
SendEmailTool.send_email(
|
OFFICIAL_WEBSITE = Config.EMAIL_OFFICIAL_WEBSITE
|
||||||
SendEmailTool.gmail_user,
|
if EXPIRE_MINUTES is None:
|
||||||
SendEmailTool.app_password,
|
EXPIRE_MINUTES = Config.VERIFICATION_CODE_EXPIRE_MINUTES
|
||||||
email,
|
htmlbody = f"""
|
||||||
subject,
|
<!DOCTYPE html>
|
||||||
body
|
<html lang="zh-CN">
|
||||||
)
|
<head>
|
||||||
logger.info(f"Verification email sent to {email}")
|
<meta charset="UTF-8">
|
||||||
|
<title>验证码邮件</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background-color:#f5f6f7;font-family:Arial,Helvetica,sans-serif;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:40px 0;">
|
||||||
|
<!-- 主体卡片 -->
|
||||||
|
<table width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;padding:32px;">
|
||||||
|
|
||||||
|
<!-- 应用名 -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="font-size:22px;font-weight:bold;color:#333333;padding-bottom:16px;">
|
||||||
|
{APP_NAME}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 操作提示 -->
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:14px;color:#555555;padding-bottom:24px;text-align:center;">
|
||||||
|
你正在进行 <strong>{ACTION_NAME}</strong> 操作,请使用以下验证码完成验证:
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 验证码 -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:20px 0;">
|
||||||
|
<div style="
|
||||||
|
display:inline-block;
|
||||||
|
font-size:32px;
|
||||||
|
font-weight:bold;
|
||||||
|
letter-spacing:6px;
|
||||||
|
color:#2e86de;
|
||||||
|
padding:12px 24px;
|
||||||
|
border:1px dashed #2e86de;
|
||||||
|
border-radius:6px;
|
||||||
|
">
|
||||||
|
{code}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 有效期说明 -->
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:13px;color:#888888;text-align:center;padding-top:16px;">
|
||||||
|
验证码有效期 {EXPIRE_MINUTES} 分钟,请勿泄露给他人。
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 0;">
|
||||||
|
<hr style="border:none;border-top:1px solid #eeeeee;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:12px;color:#999999;text-align:center;line-height:1.6;">
|
||||||
|
本邮件由 {APP_NAME} 系统自动发送,请勿回复<br>
|
||||||
|
如非本人操作,请忽略本邮件
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 官网链接 -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding-top:16px;">
|
||||||
|
<a href="{OFFICIAL_WEBSITE}"
|
||||||
|
style="font-size:12px;color:#2e86de;text-decoration:none;">
|
||||||
|
访问官网
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send email: {e}")
|
logger.error(f"Failed to send email: {e}")
|
||||||
|
|||||||
70
services/verification_code_service.py
Normal file
70
services/verification_code_service.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user