优化邮箱认证功能
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 os
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime, timedelta
|
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 flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
@ -14,17 +14,33 @@ import zipfile
|
|||||||
import shutil
|
import shutil
|
||||||
import re
|
import re
|
||||||
from pypinyin import pinyin, Style
|
from pypinyin import pinyin, Style
|
||||||
from flask import send_file
|
from flask_mail import Mail
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
from flask_migrate import Migrate
|
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
|
from config import Config
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__, static_folder='static')
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
# 初始化邮件扩展
|
||||||
|
mail = Mail(app)
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_now():
|
def inject_now():
|
||||||
return {'now': datetime.now()}
|
return {'now': datetime.now()}
|
||||||
@ -643,14 +659,20 @@ def export_pkcs12(cert_id, password):
|
|||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
|
# 检查用户是否已登录
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# 检查注册功能是否开放
|
||||||
|
if not current_app.config['REGISTRATION_OPEN']:
|
||||||
|
flash('系统暂未开放注册', 'warning')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username = request.form['username']
|
username = request.form['username'].strip()
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
confirm_password = request.form['confirm_password']
|
confirm_password = request.form['confirm_password']
|
||||||
email = request.form.get('email', '')
|
email = request.form.get('email', '').strip()
|
||||||
captcha = request.form['captcha']
|
captcha = request.form['captcha']
|
||||||
|
|
||||||
# 验证验证码
|
# 验证验证码
|
||||||
@ -663,36 +685,170 @@ def register():
|
|||||||
flash('两次输入的密码不匹配', 'danger')
|
flash('两次输入的密码不匹配', 'danger')
|
||||||
return redirect(url_for('register'))
|
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()
|
conn = get_db_connection()
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
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():
|
if cursor.fetchone():
|
||||||
flash('用户名已存在', 'danger')
|
flash('用户名或邮箱已被注册', 'danger')
|
||||||
return redirect(url_for('register'))
|
return redirect(url_for('register'))
|
||||||
|
|
||||||
|
# 根据配置决定是否立即激活账户
|
||||||
|
is_active = not current_app.config['EMAIL_VERIFICATION_REQUIRED']
|
||||||
|
verification_token = None
|
||||||
|
|
||||||
# 创建新用户
|
# 创建新用户
|
||||||
password_hash = generate_password_hash(password)
|
password_hash = generate_password_hash(password)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO users (username, password_hash, email, is_admin, is_active)
|
INSERT INTO users (username, password_hash, email, is_admin, is_active, verification_token)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
""", (username, password_hash, email, False, True))
|
""", (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()
|
conn.commit()
|
||||||
|
|
||||||
flash('注册成功,请登录', 'success')
|
if is_active:
|
||||||
return redirect(url_for('login'))
|
flash('注册成功,请登录', 'success')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('register'))
|
||||||
|
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Database error: {e}")
|
conn.rollback()
|
||||||
|
current_app.logger.error(f'数据库错误: {str(e)}')
|
||||||
flash('注册失败,请稍后再试', 'danger')
|
flash('注册失败,请稍后再试', 'danger')
|
||||||
finally:
|
finally:
|
||||||
if conn.is_connected():
|
if conn.is_connected():
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
# 生成新验证码
|
||||||
captcha_code = generate_captcha()
|
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('/')
|
@app.route('/')
|
||||||
|
|||||||
89
config.py
89
config.py
@ -1,27 +1,86 @@
|
|||||||
# config.py
|
|
||||||
import os
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 先加载环境变量(必须在Config类之前)
|
||||||
|
load_dotenv(Path(__file__).parent / '.env', override=True)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# Flask配置
|
# Flask 安全配置
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here')
|
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 = {
|
DB_CONFIG = {
|
||||||
'host': '192.168.31.11',
|
'host': os.getenv('DB_HOST', 'localhost'),
|
||||||
'database': 'cert_manager',
|
'database': os.getenv('DB_NAME', 'cert_manager'),
|
||||||
'user': 'certmgr',
|
'user': os.getenv('DB_USER', 'certmgr'),
|
||||||
'password': 'certmgr123'
|
'password': os.getenv('DB_PASSWORD', ''),
|
||||||
|
'port': int(os.getenv('DB_PORT', '3306')),
|
||||||
|
'charset': 'utf8mb4',
|
||||||
|
'collation': 'utf8mb4_general_ci',
|
||||||
|
'autocommit': True
|
||||||
}
|
}
|
||||||
|
|
||||||
# 证书存储路径
|
# 证书存储路径 (使用Path更安全)
|
||||||
CERT_STORE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cert_store')
|
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_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')
|
ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@example.com')
|
||||||
|
|
||||||
# 应用运行配置
|
# 应用运行配置
|
||||||
APP_HOST = '0.0.0.0'
|
APP_HOST = os.getenv('APP_HOST', '0.0.0.0')
|
||||||
APP_PORT = 9875
|
APP_PORT = int(os.getenv('APP_PORT', '9875'))
|
||||||
DEBUG = True
|
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),
|
email VARCHAR(100),
|
||||||
is_admin BOOLEAN DEFAULT FALSE,
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
is_active tinyint(1) DEFAULT '1',
|
is_active tinyint(1) DEFAULT '1',
|
||||||
|
verification_token VARCHAR(255),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>证书管理系统 - {% block title %}{% endblock %}</title>
|
<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://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">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@ -51,7 +52,7 @@
|
|||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand fw-bold" href="{{ url_for('index') }}">
|
<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>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
@ -137,8 +138,8 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container text-center text-muted">
|
<div class="container text-center text-muted">
|
||||||
<small>
|
<small>
|
||||||
<p class="mb-1">证书管理系统 © {{ now.year }} - 基于Flask构建</p>
|
<p class="mb-1">自签证书管理系统 © {{ now.year }} - 基于Flask构建</p>
|
||||||
<p class="mb-0">版本 1.0.0</p>
|
<p class="mb-0">版本 6.0.0</p>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -12,30 +12,60 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('register') }}">
|
<form method="POST" action="{{ url_for('register') }}" id="registerForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">
|
<label for="username" class="form-label">
|
||||||
<i class="fas fa-user me-1 text-muted"></i>用户名
|
<i class="fas fa-user me-1 text-muted"></i>用户名
|
||||||
</label>
|
</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" required
|
<input type="text" class="form-control" id="username" name="username" required
|
||||||
placeholder="4-20位字母、数字或下划线">
|
pattern="[a-zA-Z0-9_]{4,20}"
|
||||||
<small class="form-text text-muted">请输入4-20位的字母、数字或下划线</small>
|
title="4-20位字母、数字或下划线">
|
||||||
|
<small class="form-text text-muted">4-20位字母、数字或下划线</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">
|
<label for="password" class="form-label">
|
||||||
<i class="fas fa-lock me-1 text-muted"></i>密码
|
<i class="fas fa-lock me-1 text-muted"></i>密码
|
||||||
</label>
|
</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" required
|
<input type="password" class="form-control" id="password" name="password" required
|
||||||
placeholder="至少8位字符">
|
minlength="{{ config.PASSWORD_POLICY.min_length }}"
|
||||||
<small class="form-text text-muted">至少8位字符,包含字母和数字</small>
|
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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="confirm_password" class="form-label">
|
<label for="confirm_password" class="form-label">
|
||||||
<i class="fas fa-lock me-1 text-muted"></i>确认密码
|
<i class="fas fa-lock me-1 text-muted"></i>确认密码
|
||||||
</label>
|
</label>
|
||||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required
|
<input type="password" class="form-control" id="confirm_password"
|
||||||
placeholder="再次输入密码">
|
name="confirm_password" required
|
||||||
|
oninput="checkPasswordMatch()">
|
||||||
|
<small id="passwordMatchError" class="text-danger d-none">两次输入的密码不一致</small>
|
||||||
</div>
|
</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">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">
|
<label for="email" class="form-label">
|
||||||
<i class="fas fa-envelope me-1 text-muted"></i>邮箱(可选)
|
<i class="fas fa-envelope me-1 text-muted"></i>邮箱(可选)
|
||||||
@ -43,6 +73,8 @@
|
|||||||
<input type="email" class="form-control" id="email" name="email"
|
<input type="email" class="form-control" id="email" name="email"
|
||||||
placeholder="example@domain.com">
|
placeholder="example@domain.com">
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="captcha" class="form-label">
|
<label for="captcha" class="form-label">
|
||||||
<i class="fas fa-shield-alt me-1 text-muted"></i>验证码
|
<i class="fas fa-shield-alt me-1 text-muted"></i>验证码
|
||||||
@ -57,6 +89,7 @@
|
|||||||
title="点击刷新验证码">
|
title="点击刷新验证码">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-user-plus me-1"></i> 注册
|
<i class="fas fa-user-plus me-1"></i> 注册
|
||||||
@ -70,10 +103,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function refreshCaptcha() {
|
function refreshCaptcha() {
|
||||||
var captchaImage = document.getElementById('captcha-image');
|
document.getElementById('captcha-image').src = "{{ url_for('captcha') }}?" + Date.now();
|
||||||
captchaImage.src = "{{ url_for('captcha') }}?" + new Date().getTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user