From 01e546ab514fdee09e410116669a88ca2ed44737 Mon Sep 17 00:00:00 2001 From: wzj <244142824@qq.com> Date: Sat, 14 Jun 2025 19:22:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=82=AE=E7=AE=B1=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 39 +++++++++ app.py | 184 +++++++++++++++++++++++++++++++++++++--- config.py | 89 +++++++++++++++---- database.py | 1 + static/favicon.svg | 1 + templates/base.html | 7 +- templates/register.html | 79 +++++++++++++++-- 7 files changed, 359 insertions(+), 41 deletions(-) create mode 100644 .env create mode 100644 static/favicon.svg diff --git a/.env b/.env new file mode 100644 index 0000000..d77376d --- /dev/null +++ b/.env @@ -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" # 发件人显示名称 \ No newline at end of file diff --git a/app.py b/app.py index 1c520f0..6172971 100644 --- a/app.py +++ b/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""" + +
+感谢注册!
+请点击以下链接完成邮箱验证:
+ +(链接24小时内有效)
+如果链接无法点击,请复制到浏览器地址栏访问
+ + + """ + + 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/