优化邮箱认证功能
This commit is contained in:
parent
5854180c29
commit
01e546ab51
39
.env
Normal file
39
.env
Normal file
@ -0,0 +1,39 @@
|
||||
# [安全配置]
|
||||
SECRET_KEY=your-unique-and-secure-secret-key-here # Flask应用加密密钥(生产环境必须修改,建议使用32位随机字符串)
|
||||
ADMIN_USERNAME=admin # 管理员名称
|
||||
ADMIN_PASSWORD=123456 # 管理员初始密码(首次部署后必须修改)
|
||||
|
||||
# [数据库配置]
|
||||
DB_HOST=192.168.31.11 # 数据库服务器IP地址或域名
|
||||
DB_NAME=cert_manager # 数据库名称
|
||||
DB_USER=certmgr # 数据库用户名
|
||||
DB_PASSWORD=certmgr123 # 数据库密码(生产环境建议使用强密码)
|
||||
DB_PORT=3306 # 数据库端口(MySQL默认3306)
|
||||
|
||||
# [应用配置]
|
||||
APP_HOST=0.0.0.0 # 应用监听地址(0.0.0.0表示允许所有IP访问)
|
||||
APP_PORT=9875 # 应用监听端口
|
||||
DEBUG=False # 调试模式(生产环境必须关闭)
|
||||
APP_DOMAIN=localhost # 应用对外域名(用于邮件链接生成)
|
||||
APP_PROTOCOL=http # 应用协议(http或https)
|
||||
|
||||
# [注册配置]
|
||||
REGISTRATION_OPEN=True # 是否开放用户注册(True/False)
|
||||
EMAIL_VERIFICATION_REQUIRED=True # 注册是否需要邮箱验证(True/False)
|
||||
|
||||
# [密码策略]
|
||||
PASSWORD_MIN_LENGTH=6 # 密码最小长度
|
||||
PASSWORD_REQUIRE_UPPERCASE=False # 是否要求包含大写字母(True/False)
|
||||
PASSWORD_REQUIRE_LOWERCASE=False # 是否要求包含小写字母(True/False)
|
||||
PASSWORD_REQUIRE_DIGITS=True # 是否要求包含数字(True/False)
|
||||
PASSWORD_REQUIRE_SPECIAL=False # 是否要求包含特殊字符(True/False)
|
||||
|
||||
# [邮件配置]
|
||||
MAIL_SERVER=smtp.qq.com # SMTP服务器地址(QQ邮箱为smtp.qq.com)
|
||||
MAIL_PORT=465 # SMTP端口(QQ邮箱SSL端口为465)
|
||||
MAIL_USE_SSL=True # 是否使用SSL加密(QQ邮箱必须开启)
|
||||
MAIL_USE_TLS=False # 是否使用TLS加密(与SSL二选一)
|
||||
MAIL_USERNAME= # 发件邮箱地址
|
||||
MAIL_PASSWORD= # SMTP授权码(非邮箱密码)
|
||||
MAIL_DEFAULT_SENDER_EMAIL= # 默认发件邮箱
|
||||
MAIL_DEFAULT_SENDER_NAME="Certificate Manager" # 发件人显示名称
|
||||
184
app.py
184
app.py
@ -2,7 +2,7 @@
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory, Response
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory, Response, current_app
|
||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import mysql.connector
|
||||
@ -14,17 +14,33 @@ import zipfile
|
||||
import shutil
|
||||
import re
|
||||
from pypinyin import pinyin, Style
|
||||
from flask import send_file
|
||||
from flask_mail import Mail
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from flask_migrate import Migrate
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# 先加载环境变量(必须在创建app之前)
|
||||
load_dotenv(Path(__file__).parent / '.env', override=True)
|
||||
def generate_full_url(endpoint, **values):
|
||||
"""生成完整的URL,考虑自定义域名和协议"""
|
||||
base_url = f"{current_app.config['APP_PROTOCOL']}://{current_app.config['APP_DOMAIN']}"
|
||||
path = url_for(endpoint, **values)
|
||||
return urljoin(base_url, path)
|
||||
|
||||
# 从配置文件中导入配置
|
||||
from config import Config
|
||||
|
||||
app = Flask(__name__)
|
||||
app = Flask(__name__, static_folder='static')
|
||||
app.config.from_object(Config)
|
||||
|
||||
# 初始化邮件扩展
|
||||
mail = Mail(app)
|
||||
|
||||
@app.context_processor
|
||||
def inject_now():
|
||||
return {'now': datetime.now()}
|
||||
@ -643,14 +659,20 @@ def export_pkcs12(cert_id, password):
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
# 检查用户是否已登录
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# 检查注册功能是否开放
|
||||
if not current_app.config['REGISTRATION_OPEN']:
|
||||
flash('系统暂未开放注册', 'warning')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
username = request.form['username'].strip()
|
||||
password = request.form['password']
|
||||
confirm_password = request.form['confirm_password']
|
||||
email = request.form.get('email', '')
|
||||
email = request.form.get('email', '').strip()
|
||||
captcha = request.form['captcha']
|
||||
|
||||
# 验证验证码
|
||||
@ -663,36 +685,170 @@ def register():
|
||||
flash('两次输入的密码不匹配', 'danger')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
# 密码策略检查
|
||||
password_errors = []
|
||||
policy = current_app.config['PASSWORD_POLICY']
|
||||
|
||||
if len(password) < policy['min_length']:
|
||||
password_errors.append(f"密码长度至少{policy['min_length']}位")
|
||||
if policy['require_uppercase'] and not re.search(r'[A-Z]', password):
|
||||
password_errors.append("必须包含大写字母")
|
||||
if policy['require_lowercase'] and not re.search(r'[a-z]', password):
|
||||
password_errors.append("必须包含小写字母")
|
||||
if policy['require_digits'] and not re.search(r'[0-9]', password):
|
||||
password_errors.append("必须包含数字")
|
||||
if policy['require_special_chars'] and not re.search(r'[^A-Za-z0-9]', password):
|
||||
password_errors.append("必须包含特殊字符")
|
||||
|
||||
if password_errors:
|
||||
flash('密码不符合要求: ' + ', '.join(password_errors), 'danger')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
# 验证用户名是否已存在
|
||||
conn = get_db_connection()
|
||||
if conn:
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
|
||||
|
||||
# 检查用户名和邮箱是否已存在
|
||||
cursor.execute("SELECT id FROM users WHERE username = %s OR email = %s",
|
||||
(username, email))
|
||||
if cursor.fetchone():
|
||||
flash('用户名已存在', 'danger')
|
||||
flash('用户名或邮箱已被注册', 'danger')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
# 根据配置决定是否立即激活账户
|
||||
is_active = not current_app.config['EMAIL_VERIFICATION_REQUIRED']
|
||||
verification_token = None
|
||||
|
||||
# 创建新用户
|
||||
password_hash = generate_password_hash(password)
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, password_hash, email, is_admin, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""", (username, password_hash, email, False, True))
|
||||
INSERT INTO users (username, password_hash, email, is_admin, is_active, verification_token)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""", (username, password_hash, email, False, is_active, verification_token))
|
||||
|
||||
if current_app.config['EMAIL_VERIFICATION_REQUIRED']:
|
||||
# 生成验证令牌
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
|
||||
token = serializer.dumps({'username': username, 'email': email}, salt='email-verify')
|
||||
|
||||
# 更新用户验证令牌
|
||||
cursor.execute("""
|
||||
UPDATE users SET verification_token = %s WHERE username = %s
|
||||
""", (token, username))
|
||||
|
||||
# 发送验证邮件
|
||||
try:
|
||||
from flask_mail import Message
|
||||
msg = Message('请验证您的邮箱 - 自签证书管理系统',
|
||||
sender=current_app.config['MAIL_DEFAULT_SENDER'],
|
||||
recipients=[email])
|
||||
|
||||
confirm_url = generate_full_url('verify_email', token=token)
|
||||
|
||||
# 使用HTML格式的邮件内容
|
||||
msg.html = f"""
|
||||
<html>
|
||||
<body>
|
||||
<p>感谢注册!</p>
|
||||
<p>请点击以下链接完成邮箱验证:</p>
|
||||
<p><a href="{confirm_url}">{confirm_url}</a></p>
|
||||
<p>(链接24小时内有效)</p>
|
||||
<p>如果链接无法点击,请复制到浏览器地址栏访问</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
mail = Mail(current_app)
|
||||
mail.send(msg)
|
||||
flash('验证邮件已发送至您的邮箱,请查收并完成验证', 'info')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'邮件发送失败: {str(e)}')
|
||||
conn.rollback()
|
||||
flash('发送验证邮件失败,请稍后再试或联系管理员', 'danger')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
conn.commit()
|
||||
|
||||
flash('注册成功,请登录', 'success')
|
||||
return redirect(url_for('login'))
|
||||
if is_active:
|
||||
flash('注册成功,请登录', 'success')
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
return redirect(url_for('register'))
|
||||
|
||||
except Error as e:
|
||||
print(f"Database error: {e}")
|
||||
conn.rollback()
|
||||
current_app.logger.error(f'数据库错误: {str(e)}')
|
||||
flash('注册失败,请稍后再试', 'danger')
|
||||
finally:
|
||||
if conn.is_connected():
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 生成新验证码
|
||||
captcha_code = generate_captcha()
|
||||
return render_template('register.html', captcha_code=captcha_code)
|
||||
return render_template('register.html',
|
||||
captcha_code=captcha_code,
|
||||
registration_open=current_app.config['REGISTRATION_OPEN'],
|
||||
email_required=current_app.config['EMAIL_VERIFICATION_REQUIRED'])
|
||||
|
||||
|
||||
@app.route('/verify-email/<token>')
|
||||
def verify_email(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(generate_full_url('index'))
|
||||
|
||||
try:
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
|
||||
data = serializer.loads(token, salt='email-verify', max_age=86400) # 24小时有效期
|
||||
|
||||
username = data['username']
|
||||
email = data['email']
|
||||
|
||||
conn = get_db_connection()
|
||||
if conn:
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 验证令牌是否匹配
|
||||
cursor.execute("""
|
||||
SELECT id FROM users
|
||||
WHERE username = %s AND email = %s AND verification_token = %s
|
||||
""", (username, email, token))
|
||||
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
flash('无效的验证链接', 'danger')
|
||||
return redirect(generate_full_url('login'))
|
||||
|
||||
# 激活账户
|
||||
cursor.execute("""
|
||||
UPDATE users
|
||||
SET is_active = TRUE, verification_token = NULL
|
||||
WHERE id = %s
|
||||
""", (user['id'],))
|
||||
conn.commit()
|
||||
|
||||
flash('邮箱验证成功,您现在可以登录了', 'success')
|
||||
return redirect(generate_full_url('login'))
|
||||
|
||||
except Error as e:
|
||||
conn.rollback()
|
||||
current_app.logger.error(f'数据库错误: {str(e)}')
|
||||
flash('验证过程中出现错误', 'danger')
|
||||
finally:
|
||||
if conn.is_connected():
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'令牌验证失败: {str(e)}')
|
||||
flash('验证链接无效或已过期', 'danger')
|
||||
|
||||
return redirect(generate_full_url('login'))
|
||||
|
||||
# 路由定义
|
||||
@app.route('/')
|
||||
|
||||
89
config.py
89
config.py
@ -1,27 +1,86 @@
|
||||
# config.py
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
# 先加载环境变量(必须在Config类之前)
|
||||
load_dotenv(Path(__file__).parent / '.env', override=True)
|
||||
|
||||
class Config:
|
||||
# Flask配置
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here')
|
||||
# Flask 安全配置
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key') # 生产环境必须覆盖
|
||||
SESSION_COOKIE_SECURE = True # 只允许HTTPS传输
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
PERMANENT_SESSION_LIFETIME = 3600 # 1小时会话有效期
|
||||
|
||||
# 数据库配置
|
||||
# 数据库配置 (从环境变量读取)
|
||||
DB_CONFIG = {
|
||||
'host': '192.168.31.11',
|
||||
'database': 'cert_manager',
|
||||
'user': 'certmgr',
|
||||
'password': 'certmgr123'
|
||||
'host': os.getenv('DB_HOST', 'localhost'),
|
||||
'database': os.getenv('DB_NAME', 'cert_manager'),
|
||||
'user': os.getenv('DB_USER', 'certmgr'),
|
||||
'password': os.getenv('DB_PASSWORD', ''),
|
||||
'port': int(os.getenv('DB_PORT', '3306')),
|
||||
'charset': 'utf8mb4',
|
||||
'collation': 'utf8mb4_general_ci',
|
||||
'autocommit': True
|
||||
}
|
||||
|
||||
# 证书存储路径
|
||||
CERT_STORE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cert_store')
|
||||
# 证书存储路径 (使用Path更安全)
|
||||
CERT_STORE = Path(os.getenv('CERT_STORE',
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cert_store')))
|
||||
|
||||
# 管理员初始凭据
|
||||
# 确保证书存储目录存在
|
||||
if not CERT_STORE.exists():
|
||||
CERT_STORE.mkdir(mode=0o700, parents=True) # 设置严格权限
|
||||
|
||||
# 管理员配置
|
||||
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
|
||||
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', '123456')
|
||||
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', '') # 生产环境必须设置
|
||||
ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@example.com')
|
||||
|
||||
# 应用运行配置
|
||||
APP_HOST = '0.0.0.0'
|
||||
APP_PORT = 9875
|
||||
DEBUG = True
|
||||
APP_HOST = os.getenv('APP_HOST', '0.0.0.0')
|
||||
APP_PORT = int(os.getenv('APP_PORT', '9875'))
|
||||
DEBUG = os.getenv('DEBUG', 'False') == 'True' # 生产环境应为False
|
||||
|
||||
# 注册相关配置
|
||||
REGISTRATION_OPEN = os.getenv('REGISTRATION_OPEN', 'False') == 'True'
|
||||
EMAIL_VERIFICATION_REQUIRED = os.getenv('EMAIL_VERIFICATION_REQUIRED', 'True') == 'True'
|
||||
|
||||
# 密码策略配置
|
||||
PASSWORD_POLICY = {
|
||||
'min_length': int(os.getenv('PASSWORD_MIN_LENGTH', '8')),
|
||||
'require_uppercase': os.getenv('PASSWORD_REQUIRE_UPPERCASE', 'True') == 'True',
|
||||
'require_lowercase': os.getenv('PASSWORD_REQUIRE_LOWERCASE', 'True') == 'True',
|
||||
'require_digits': os.getenv('PASSWORD_REQUIRE_DIGITS', 'True') == 'True',
|
||||
'require_special_chars': os.getenv('PASSWORD_REQUIRE_SPECIAL', 'True') == 'True'
|
||||
}
|
||||
|
||||
# 邮件服务器配置
|
||||
MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.qq.com')
|
||||
MAIL_PORT = int(os.getenv('MAIL_PORT', '465'))
|
||||
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'True') == 'True'
|
||||
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'False') == 'True'
|
||||
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
||||
MAIL_DEFAULT_SENDER = (
|
||||
os.getenv('MAIL_DEFAULT_SENDER_EMAIL', 'noreply@example.com'),
|
||||
os.getenv('MAIL_DEFAULT_SENDER_NAME', 'Certificate Manager')
|
||||
)
|
||||
|
||||
# 应用URL配置
|
||||
APP_DOMAIN = os.getenv('APP_DOMAIN', 'xunxian.liuyan.wang')
|
||||
APP_PROTOCOL = os.getenv('APP_PROTOCOL', 'https')
|
||||
SERVER_NAME = os.getenv('SERVER_NAME') # 用于URL生成
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
|
||||
LOG_FILE = os.getenv('LOG_FILE', 'app.log')
|
||||
|
||||
@property
|
||||
def SQLALCHEMY_DATABASE_URI(self):
|
||||
return f"mysql+pymysql://{self.DB_CONFIG['user']}:{self.DB_CONFIG['password']}@" \
|
||||
f"{self.DB_CONFIG['host']}:{self.DB_CONFIG['port']}/{self.DB_CONFIG['database']}"
|
||||
|
||||
@property
|
||||
def BASE_URL(self):
|
||||
return f"{self.APP_PROTOCOL}://{self.APP_DOMAIN}"
|
||||
@ -33,6 +33,7 @@ def create_database():
|
||||
email VARCHAR(100),
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
is_active tinyint(1) DEFAULT '1',
|
||||
verification_token VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
|
||||
1
static/favicon.svg
Normal file
1
static/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749894447194" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4365" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M640 768.042667c35.626667 26.794667 80 42.666667 128 42.666666a212.394667 212.394667 0 0 0 128-42.581333v138.496a32 32 0 0 1-46.506667 28.544l-3.84-2.346667L768 878.549333l-77.653333 54.272a32 32 0 0 1-50.005334-21.76l-0.298666-4.437333L640 768z m181.333333-639.872a117.333333 117.333333 0 0 1 117.12 110.208l0.213334 7.125333 0.042666 223.829333a214.442667 214.442667 0 0 0-64-56.746666L874.666667 245.461333a53.333333 53.333333 0 0 0-47.872-53.034666l-5.461334-0.298667H202.666667a53.333333 53.333333 0 0 0-53.077334 47.872l-0.256 5.461333v405.333334c0 27.605333 20.992 50.346667 47.872 53.077333l5.461334 0.256h380.586666c4.266667 7.338667 8.96 14.421333 14.08 21.205333v42.794667H202.666667a117.333333 117.333333 0 0 1-117.12-110.165333L85.333333 650.837333v-405.333333A117.333333 117.333333 0 0 1 195.498667 128.426667l7.168-0.213334h618.666666zM768 426.666667a170.666667 170.666667 0 1 1 0 341.376A170.666667 170.666667 0 0 1 768 426.666667z m-288 106.666666a32 32 0 0 1 4.352 63.701334L480 597.333333h-192a32 32 0 0 1-4.352-63.701333l4.352-0.298667h192z m256-234.666666a32 32 0 0 1 4.352 63.701333l-4.352 0.298667H288a32 32 0 0 1-4.352-63.701334L288 298.666667h448z" fill="#212121" p-id="4366"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>证书管理系统 - {% block title %}{% endblock %}</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@ -51,7 +52,7 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="{{ url_for('index') }}">
|
||||
<i class="fas fa-shield-alt me-2"></i>证书管理系统
|
||||
<i class="fas fa-shield-alt me-2"></i>自签证书管理系统
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -137,8 +138,8 @@
|
||||
<footer class="footer">
|
||||
<div class="container text-center text-muted">
|
||||
<small>
|
||||
<p class="mb-1">证书管理系统 © {{ now.year }} - 基于Flask构建</p>
|
||||
<p class="mb-0">版本 1.0.0</p>
|
||||
<p class="mb-1">自签证书管理系统 © {{ now.year }} - 基于Flask构建</p>
|
||||
<p class="mb-0">版本 6.0.0</p>
|
||||
</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -12,30 +12,60 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('register') }}">
|
||||
<form method="POST" action="{{ url_for('register') }}" id="registerForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user me-1 text-muted"></i>用户名
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required
|
||||
placeholder="4-20位字母、数字或下划线">
|
||||
<small class="form-text text-muted">请输入4-20位的字母、数字或下划线</small>
|
||||
pattern="[a-zA-Z0-9_]{4,20}"
|
||||
title="4-20位字母、数字或下划线">
|
||||
<small class="form-text text-muted">4-20位字母、数字或下划线</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock me-1 text-muted"></i>密码
|
||||
</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required
|
||||
placeholder="至少8位字符">
|
||||
<small class="form-text text-muted">至少8位字符,包含字母和数字</small>
|
||||
minlength="{{ config.PASSWORD_POLICY.min_length }}"
|
||||
pattern="{% if config.PASSWORD_POLICY.require_uppercase %}(?=.*[A-Z]){% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_lowercase %}(?=.*[a-z]){% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_digits %}(?=.*\d]){% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_special_chars %}(?=.*[!@#$%^&*]){% endif %}.*"
|
||||
title="{% if config.PASSWORD_POLICY.require_uppercase %}必须包含大写字母{% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_lowercase %}必须包含小写字母{% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_digits %}必须包含数字{% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_special_chars %}必须包含特殊字符{% endif %}">
|
||||
<small class="form-text text-muted">
|
||||
密码要求:至少{{ config.PASSWORD_POLICY.min_length }}位
|
||||
{% if config.PASSWORD_POLICY.require_uppercase %},包含大写字母{% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_lowercase %},包含小写字母{% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_digits %},包含数字{% endif %}
|
||||
{% if config.PASSWORD_POLICY.require_special_chars %},包含特殊字符(!@#$%^&*){% endif %}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">
|
||||
<i class="fas fa-lock me-1 text-muted"></i>确认密码
|
||||
</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required
|
||||
placeholder="再次输入密码">
|
||||
<input type="password" class="form-control" id="confirm_password"
|
||||
name="confirm_password" required
|
||||
oninput="checkPasswordMatch()">
|
||||
<small id="passwordMatchError" class="text-danger d-none">两次输入的密码不一致</small>
|
||||
</div>
|
||||
|
||||
{% if config.EMAIL_VERIFICATION_REQUIRED %}
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope me-1 text-muted"></i>邮箱
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required
|
||||
placeholder="example@domain.com">
|
||||
<small class="form-text text-muted">用于接收验证邮件</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope me-1 text-muted"></i>邮箱(可选)
|
||||
@ -43,6 +73,8 @@
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
placeholder="example@domain.com">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="captcha" class="form-label">
|
||||
<i class="fas fa-shield-alt me-1 text-muted"></i>验证码
|
||||
@ -57,6 +89,7 @@
|
||||
title="点击刷新验证码">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-user-plus me-1"></i> 注册
|
||||
@ -70,10 +103,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function refreshCaptcha() {
|
||||
var captchaImage = document.getElementById('captcha-image');
|
||||
captchaImage.src = "{{ url_for('captcha') }}?" + new Date().getTime();
|
||||
document.getElementById('captcha-image').src = "{{ url_for('captcha') }}?" + Date.now();
|
||||
}
|
||||
|
||||
function checkPasswordMatch() {
|
||||
const password = document.getElementById('password');
|
||||
const confirmPassword = document.getElementById('confirm_password');
|
||||
const errorElement = document.getElementById('passwordMatchError');
|
||||
|
||||
if (password.value !== confirmPassword.value) {
|
||||
errorElement.classList.remove('d-none');
|
||||
confirmPassword.setCustomValidity("密码不匹配");
|
||||
} else {
|
||||
errorElement.classList.add('d-none');
|
||||
confirmPassword.setCustomValidity("");
|
||||
}
|
||||
}
|
||||
|
||||
// 实时验证密码复杂度
|
||||
document.getElementById('password').addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const policy = {
|
||||
minLength: {{ config.PASSWORD_POLICY.min_length }},
|
||||
requireUpper: {{ config.PASSWORD_POLICY.require_uppercase|lower }},
|
||||
requireLower: {{ config.PASSWORD_POLICY.require_lowercase|lower }},
|
||||
requireDigit: {{ config.PASSWORD_POLICY.require_digits|lower }},
|
||||
requireSpecial: {{ config.PASSWORD_POLICY.require_special_chars|lower }}
|
||||
};
|
||||
|
||||
// 可以在这里添加更复杂的实时验证逻辑
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user