first commit

This commit is contained in:
wzj 2025-06-14 08:47:23 +08:00
commit 37610aeb2c
26 changed files with 1890 additions and 0 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
FLASK_SECRET_KEY=your-secret-key
DB_HOST=192.168.31.11
DB_NAME=cert_manager
DB_USER=root
DB_PASSWORD=Home123#$.

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/certmgr.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

28
.idea/deployment.xml generated Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="root@192.168.31.10:22 password">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="root@192.168.31.10:22 password (2)">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="root@192.168.31.11:22 password">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

View File

@ -0,0 +1,16 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="APScheduler" />
<item index="1" class="java.lang.String" itemvalue="Flask" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/certmgr.iml" filepath="$PROJECT_DIR$/.idea/certmgr.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

Binary file not shown.

951
app.py Normal file
View File

@ -0,0 +1,951 @@
import os
import subprocess
from datetime import datetime, timedelta
from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory
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
from mysql.connector import Error
import random
import string
import uuid
app = Flask(__name__)
app.secret_key = 'your-secret-key-here'
# 数据库配置
db_config = {
'host': '192.168.31.11',
'database': 'cert_manager',
'user': 'root',
'password': 'Home123#$.'
}
# Flask-Login 配置
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
# 确保证书存储目录存在
CERT_STORE = os.path.join(os.path.dirname(__file__), 'cert_store')
os.makedirs(CERT_STORE, exist_ok=True)
class User(UserMixin):
pass
@login_manager.user_loader
def load_user(user_id):
try:
conn = mysql.connector.connect(**db_config)
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user_data = cursor.fetchone()
if user_data:
user = User()
user.id = user_data['id']
user.username = user_data['username']
user.is_admin = user_data['is_admin']
return user
return None
except Error as e:
print(f"Database error: {e}")
return None
finally:
if conn.is_connected():
cursor.close()
conn.close()
# 辅助函数
def get_db_connection():
try:
conn = mysql.connector.connect(**db_config)
return conn
except Error as e:
print(f"Database connection error: {e}")
return None
def generate_captcha():
# 生成6位随机验证码
captcha_code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
# 清除旧的验证码
cursor.execute("DELETE FROM captcha WHERE created_at < NOW() - INTERVAL 10 MINUTE")
# 插入新验证码
cursor.execute("INSERT INTO captcha (code) VALUES (%s)", (captcha_code,))
conn.commit()
return captcha_code
except Error as e:
print(f"Database error: {e}")
return None
finally:
if conn.is_connected():
cursor.close()
conn.close()
return None
def verify_captcha(user_input):
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
cursor.execute("SELECT code FROM captcha ORDER BY created_at DESC LIMIT 1")
result = cursor.fetchone()
if result and user_input.upper() == result[0]:
return True
return False
except Error as e:
print(f"Database error: {e}")
return False
finally:
if conn.is_connected():
cursor.close()
conn.close()
return False
def create_ca(ca_name, common_name, organization, organizational_unit, country, state, locality, key_size, days_valid,
created_by):
# 创建CA目录
ca_dir = os.path.join(CERT_STORE, f"ca_{ca_name}")
os.makedirs(ca_dir, exist_ok=True)
key_path = os.path.join(ca_dir, f"{common_name}.key")
cert_path = os.path.join(ca_dir, f"{common_name}.crt")
# 生成CA私钥
subprocess.run([
'openssl', 'genrsa', '-out', key_path, str(key_size)
], check=True)
# 生成CA自签名证书
subprocess.run([
'openssl', 'req', '-new', '-x509', '-days', str(days_valid),
'-key', key_path, '-out', cert_path,
'-subj', f'/CN={common_name}/O={organization}/OU={organizational_unit}/C={country}/ST={state}/L={locality}'
], check=True)
# 保存到数据库
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO certificate_authorities
(name, common_name, organization, organizational_unit, country, state, locality,
key_size, days_valid, cert_path, key_path, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (ca_name, common_name, organization, organizational_unit, country, state, locality,
key_size, days_valid, cert_path, key_path, created_by))
conn.commit()
return cursor.lastrowid
except Error as e:
print(f"Database error: {e}")
return None
finally:
if conn.is_connected():
cursor.close()
conn.close()
return None
def create_certificate(ca_id, common_name, san_dns, san_ip, organization, organizational_unit,
country, state, locality, key_size, days_valid, created_by):
# 获取CA信息
ca = get_ca_by_id(ca_id)
if not ca:
return None
# 创建证书目录
cert_dir = os.path.join(CERT_STORE, f"certs_{common_name}")
os.makedirs(cert_dir, exist_ok=True)
key_path = os.path.join(cert_dir, f"{common_name}.key")
csr_path = os.path.join(cert_dir, f"{common_name}.csr")
cert_path = os.path.join(cert_dir, f"{common_name}.crt")
# 生成私钥
subprocess.run([
'openssl', 'genrsa', '-out', key_path, str(key_size)
], check=True)
# 创建CSR配置文件
csr_config = f"""
[req]
default_bits = {key_size}
prompt = no
default_md = sha256
distinguished_name = dn
[dn]
CN = {common_name}
O = {organization}
OU = {organizational_unit}
C = {country}
ST = {state}
L = {locality}
"""
if san_dns or san_ip:
csr_config += "\nreq_extensions = req_ext\n[req_ext]\nsubjectAltName = @alt_names\n[alt_names]\n"
if san_dns:
dns_entries = san_dns.split(',')
for i, dns in enumerate(dns_entries, 1):
csr_config += f"DNS.{i} = {dns.strip()}\n"
if san_ip:
ip_entries = san_ip.split(',')
for i, ip in enumerate(ip_entries, 1):
csr_config += f"IP.{i} = {ip.strip()}\n"
config_path = os.path.join(cert_dir, 'csr_config.cnf')
with open(config_path, 'w') as f:
f.write(csr_config)
# 生成CSR
subprocess.run([
'openssl', 'req', '-new', '-key', key_path, '-out', csr_path,
'-config', config_path
], check=True)
# 使用CA签名证书
subprocess.run([
'openssl', 'x509', '-req', '-in', csr_path, '-CA', ca['cert_path'],
'-CAkey', ca['key_path'], '-CAcreateserial', '-out', cert_path,
'-days', str(days_valid), '-sha256'
], check=True)
# 计算过期时间
expires_at = datetime.now() + timedelta(days=days_valid)
# 保存到数据库
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO certificates
(common_name, san_dns, san_ip, organization, organizational_unit, country, state, locality,
key_size, days_valid, cert_path, key_path, csr_path, ca_id, created_by, expires_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (common_name, san_dns, san_ip, organization, organizational_unit, country, state, locality,
key_size, days_valid, cert_path, key_path, csr_path, ca_id, created_by, expires_at))
conn.commit()
return cursor.lastrowid
except Error as e:
print(f"Database error: {e}")
return None
finally:
if conn.is_connected():
cursor.close()
conn.close()
return None
def get_ca_by_id(ca_id):
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM certificate_authorities WHERE id = %s", (ca_id,))
return cursor.fetchone()
except Error as e:
print(f"Database error: {e}")
return None
finally:
if conn.is_connected():
cursor.close()
conn.close()
return None
def get_certificate_by_id(cert_id):
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM certificates WHERE id = %s", (cert_id,))
return cursor.fetchone()
except Error as e:
print(f"Database error: {e}")
return None
finally:
if conn.is_connected():
cursor.close()
conn.close()
return None
def revoke_certificate(cert_id, reason):
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
cursor.execute("""
UPDATE certificates
SET status = 'revoked', revoked_at = NOW(), revocation_reason = %s
WHERE id = %s
""", (reason, cert_id))
conn.commit()
return True
except Error as e:
print(f"Database error: {e}")
return False
finally:
if conn.is_connected():
cursor.close()
conn.close()
return False
def renew_certificate(cert_id, days_valid):
cert = get_certificate_by_id(cert_id)
if not cert:
return False
ca = get_ca_by_id(cert['ca_id'])
if not ca:
return False
# 使用现有CSR重新签名
new_cert_path = os.path.join(os.path.dirname(cert['cert_path']), f"renewed_{cert['common_name']}.crt")
subprocess.run([
'openssl', 'x509', '-req', '-in', cert['csr_path'], '-CA', ca['cert_path'],
'-CAkey', ca['key_path'], '-CAcreateserial', '-out', new_cert_path,
'-days', str(days_valid), '-sha256'
], check=True)
# 更新数据库
new_expires_at = datetime.now() + timedelta(days=days_valid)
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
cursor.execute("""
UPDATE certificates
SET cert_path = %s, days_valid = %s, expires_at = %s, status = 'active',
revoked_at = NULL, revocation_reason = NULL
WHERE id = %s
""", (new_cert_path, days_valid, new_expires_at, cert_id))
conn.commit()
return True
except Error as e:
print(f"Database error: {e}")
return False
finally:
if conn.is_connected():
cursor.close()
conn.close()
return False
def generate_crl(ca_id):
ca = get_ca_by_id(ca_id)
if not ca:
return False
crl_path = os.path.join(os.path.dirname(ca['cert_path']), f"crl_{ca['name']}.pem")
# 获取所有被吊销的证书
revoked_certs = []
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT cert_path FROM certificates
WHERE ca_id = %s AND status = 'revoked'
""", (ca_id,))
revoked_certs = [row['cert_path'] for row in cursor.fetchall()]
except Error as e:
print(f"Database error: {e}")
return False
finally:
if conn.is_connected():
cursor.close()
conn.close()
# 创建索引文件
index_file = os.path.join(os.path.dirname(ca['cert_path']), 'index.txt')
if not os.path.exists(index_file):
open(index_file, 'w').close()
# 生成CRL
subprocess.run([
'openssl', 'ca', '-gencrl', '-out', crl_path,
'-keyfile', ca['key_path'], '-cert', ca['cert_path'],
'-crldays', '30'
], check=True)
# 更新数据库
next_update = datetime.now() + timedelta(days=30)
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
# 检查是否已有CRL记录
cursor.execute("SELECT id FROM certificate_revocation_list WHERE ca_id = %s", (ca_id,))
if cursor.fetchone():
cursor.execute("""
UPDATE certificate_revocation_list
SET crl_path = %s, last_updated = NOW(), next_update = %s
WHERE ca_id = %s
""", (crl_path, next_update, ca_id))
else:
cursor.execute("""
INSERT INTO certificate_revocation_list
(ca_id, crl_path, next_update)
VALUES (%s, %s, %s)
""", (ca_id, crl_path, next_update))
conn.commit()
return True
except Error as e:
print(f"Database error: {e}")
return False
finally:
if conn.is_connected():
cursor.close()
conn.close()
return False
def export_pkcs12(cert_id, password):
cert = get_certificate_by_id(cert_id)
if not cert:
return None
ca = get_ca_by_id(cert['ca_id'])
if not ca:
return None
pkcs12_path = os.path.join(os.path.dirname(cert['cert_path']), f"{cert['common_name']}.p12")
subprocess.run([
'openssl', 'pkcs12', '-export', '-out', pkcs12_path,
'-inkey', cert['key_path'], '-in', cert['cert_path'],
'-certfile', ca['cert_path'], '-passout', f'pass:{password}'
], check=True)
return pkcs12_path
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
confirm_password = request.form['confirm_password']
email = request.form.get('email', '')
captcha = request.form['captcha']
# 验证验证码
if not verify_captcha(captcha):
flash('验证码错误', 'danger')
return redirect(url_for('register'))
# 验证密码匹配
if password != confirm_password:
flash('两次输入的密码不匹配', '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,))
if cursor.fetchone():
flash('用户名已存在', 'danger')
return redirect(url_for('register'))
# 创建新用户
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))
conn.commit()
flash('注册成功,请登录', 'success')
return redirect(url_for('login'))
except Error as e:
print(f"Database error: {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)
# 路由定义
@app.route('/')
@login_required
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
captcha = request.form['captcha']
if not verify_captcha(captcha):
flash('验证码错误', 'danger')
return redirect(url_for('login'))
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT * FROM users
WHERE username = %s AND is_active = TRUE
""", (username,))
user_data = cursor.fetchone()
if user_data and check_password_hash(user_data['password_hash'], password):
user = User()
user.id = user_data['id']
user.username = user_data['username']
user.is_admin = user_data['is_admin']
login_user(user)
flash('登录成功', 'success')
return redirect(url_for('index'))
else:
flash('用户名或密码错误,或账户未激活', 'danger')
except Error as e:
print(f"Database error: {e}")
flash('登录失败,请稍后再试', 'danger')
finally:
if conn.is_connected():
cursor.close()
conn.close()
captcha_code = generate_captcha()
return render_template('login.html', captcha_code=captcha_code)
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('您已成功登出', 'success')
return redirect(url_for('login'))
@app.route('/cas')
@login_required
def ca_list():
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
if current_user.is_admin:
cursor.execute("SELECT * FROM certificate_authorities")
else:
cursor.execute("SELECT * FROM certificate_authorities WHERE created_by = %s", (current_user.id,))
cas = cursor.fetchall()
return render_template('ca_list.html', cas=cas)
except Error as e:
print(f"Database error: {e}")
flash('获取CA列表失败', 'danger')
return redirect(url_for('index'))
finally:
if conn.is_connected():
cursor.close()
conn.close()
return redirect(url_for('index'))
@app.route('/cas/create', methods=['GET', 'POST'])
@login_required
def create_ca_view():
if request.method == 'POST':
ca_name = request.form['ca_name']
common_name = request.form['common_name']
organization = request.form['organization']
organizational_unit = request.form['organizational_unit']
country = request.form['country']
state = request.form['state']
locality = request.form['locality']
key_size = int(request.form['key_size'])
days_valid = int(request.form['days_valid'])
ca_id = create_ca(ca_name, common_name, organization, organizational_unit,
country, state, locality, key_size, days_valid, current_user.id)
if ca_id:
flash('CA创建成功', 'success')
return redirect(url_for('ca_list'))
else:
flash('CA创建失败', 'danger')
return render_template('create_ca.html')
@app.route('/cas/<int:ca_id>')
@login_required
def ca_detail(ca_id):
ca = get_ca_by_id(ca_id)
if not ca:
flash('CA不存在', 'danger')
return redirect(url_for('ca_list'))
# 检查权限
if not current_user.is_admin and ca['created_by'] != current_user.id:
flash('无权访问此CA', 'danger')
return redirect(url_for('ca_list'))
# 获取该CA颁发的证书
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT * FROM certificates
WHERE ca_id = %s
ORDER BY created_at DESC
""", (ca_id,))
certificates = cursor.fetchall()
# 获取CRL信息
cursor.execute("""
SELECT * FROM certificate_revocation_list
WHERE ca_id = %s
""", (ca_id,))
crl = cursor.fetchone()
return render_template('ca_detail.html', ca=ca, certificates=certificates, crl=crl)
except Error as e:
print(f"Database error: {e}")
flash('获取CA详情失败', 'danger')
return redirect(url_for('ca_list'))
finally:
if conn.is_connected():
cursor.close()
conn.close()
return redirect(url_for('ca_list'))
@app.route('/cas/<int:ca_id>/generate_crl')
@login_required
def generate_crl_view(ca_id):
ca = get_ca_by_id(ca_id)
if not ca:
flash('CA不存在', 'danger')
return redirect(url_for('ca_list'))
# 检查权限
if not current_user.is_admin and ca['created_by'] != current_user.id:
flash('无权操作此CA', 'danger')
return redirect(url_for('ca_list'))
if generate_crl(ca_id):
flash('CRL生成成功', 'success')
else:
flash('CRL生成失败', 'danger')
return redirect(url_for('ca_detail', ca_id=ca_id))
@app.route('/cas/<int:ca_id>/download_crl')
@login_required
def download_crl(ca_id):
ca = get_ca_by_id(ca_id)
if not ca:
flash('CA不存在', 'danger')
return redirect(url_for('ca_list'))
# 检查权限
if not current_user.is_admin and ca['created_by'] != current_user.id:
flash('无权操作此CA', 'danger')
return redirect(url_for('ca_list'))
# 获取CRL路径
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT crl_path FROM certificate_revocation_list
WHERE ca_id = %s
""", (ca_id,))
crl = cursor.fetchone()
if crl and os.path.exists(crl['crl_path']):
return send_from_directory(
os.path.dirname(crl['crl_path']),
os.path.basename(crl['crl_path']),
as_attachment=True
)
else:
flash('CRL文件不存在', 'danger')
return redirect(url_for('ca_detail', ca_id=ca_id))
except Error as e:
print(f"Database error: {e}")
flash('获取CRL失败', 'danger')
return redirect(url_for('ca_detail', ca_id=ca_id))
finally:
if conn.is_connected():
cursor.close()
conn.close()
return redirect(url_for('ca_detail', ca_id=ca_id))
@app.route('/certificates')
@login_required
def certificate_list():
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
if current_user.is_admin:
cursor.execute("""
SELECT c.*, ca.name as ca_name
FROM certificates c
JOIN certificate_authorities ca ON c.ca_id = ca.id
ORDER BY c.created_at DESC
""")
else:
cursor.execute("""
SELECT c.*, ca.name as ca_name
FROM certificates c
JOIN certificate_authorities ca ON c.ca_id = ca.id
WHERE c.created_by = %s
ORDER BY c.created_at DESC
""", (current_user.id,))
certificates = cursor.fetchall()
return render_template('certificate_list.html', certificates=certificates)
except Error as e:
print(f"Database error: {e}")
flash('获取证书列表失败', 'danger')
return redirect(url_for('index'))
finally:
if conn.is_connected():
cursor.close()
conn.close()
return redirect(url_for('index'))
@app.route('/certificates/create', methods=['GET', 'POST'])
@login_required
def create_certificate_view():
if request.method == 'POST':
common_name = request.form['common_name']
san_dns = request.form.get('san_dns', '')
san_ip = request.form.get('san_ip', '')
organization = request.form['organization']
organizational_unit = request.form['organizational_unit']
country = request.form['country']
state = request.form['state']
locality = request.form['locality']
key_size = int(request.form['key_size'])
days_valid = int(request.form['days_valid'])
ca_id = int(request.form['ca_id'])
cert_id = create_certificate(ca_id, common_name, san_dns, san_ip, organization,
organizational_unit, country, state, locality,
key_size, days_valid, current_user.id)
if cert_id:
flash('证书创建成功', 'success')
return redirect(url_for('certificate_detail', cert_id=cert_id))
else:
flash('证书创建失败', 'danger')
# 获取可用的CA
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
if current_user.is_admin:
cursor.execute("SELECT id, name FROM certificate_authorities")
else:
cursor.execute("""
SELECT id, name FROM certificate_authorities
WHERE created_by = %s
""", (current_user.id,))
cas = cursor.fetchall()
return render_template('create_certificate.html', cas=cas)
except Error as e:
print(f"Database error: {e}")
flash('获取CA列表失败', 'danger')
return redirect(url_for('certificate_list'))
finally:
if conn.is_connected():
cursor.close()
conn.close()
return redirect(url_for('certificate_list'))
@app.route('/certificates/<int:cert_id>')
@login_required
def certificate_detail(cert_id):
cert = get_certificate_by_id(cert_id)
if not cert:
flash('证书不存在', 'danger')
return redirect(url_for('certificate_list'))
# 检查权限
if not current_user.is_admin and cert['created_by'] != current_user.id:
flash('无权访问此证书', 'danger')
return redirect(url_for('certificate_list'))
# 获取CA信息
ca = get_ca_by_id(cert['ca_id'])
return render_template('certificate_detail.html', cert=cert, ca=ca)
@app.route('/certificates/<int:cert_id>/revoke', methods=['GET', 'POST'])
@login_required
def revoke_certificate_view(cert_id):
cert = get_certificate_by_id(cert_id)
if not cert:
flash('证书不存在', 'danger')
return redirect(url_for('certificate_list'))
# 检查权限
if not current_user.is_admin and cert['created_by'] != current_user.id:
flash('无权操作此证书', 'danger')
return redirect(url_for('certificate_list'))
if cert['status'] == 'revoked':
flash('证书已被吊销', 'warning')
return redirect(url_for('certificate_detail', cert_id=cert_id))
if request.method == 'POST':
reason = request.form['reason']
if revoke_certificate(cert_id, reason):
flash('证书吊销成功', 'success')
# 更新CRL
generate_crl(cert['ca_id'])
return redirect(url_for('certificate_detail', cert_id=cert_id))
else:
flash('证书吊销失败', 'danger')
return render_template('revoke_certificate.html', cert=cert)
@app.route('/certificates/<int:cert_id>/renew', methods=['GET', 'POST'])
@login_required
def renew_certificate_view(cert_id):
cert = get_certificate_by_id(cert_id)
if not cert:
flash('证书不存在', 'danger')
return redirect(url_for('certificate_list'))
# 检查权限
if not current_user.is_admin and cert['created_by'] != current_user.id:
flash('无权操作此证书', 'danger')
return redirect(url_for('certificate_list'))
if request.method == 'POST':
days_valid = int(request.form['days_valid'])
if renew_certificate(cert_id, days_valid):
flash('证书续期成功', 'success')
return redirect(url_for('certificate_detail', cert_id=cert_id))
else:
flash('证书续期失败', 'danger')
return render_template('renew_certificate.html', cert=cert)
@app.route('/certificates/<int:cert_id>/export', methods=['GET', 'POST'])
@login_required
def export_certificate_view(cert_id):
cert = get_certificate_by_id(cert_id)
if not cert:
flash('证书不存在', 'danger')
return redirect(url_for('certificate_list'))
# 检查权限
if not current_user.is_admin and cert['created_by'] != current_user.id:
flash('无权操作此证书', 'danger')
return redirect(url_for('certificate_list'))
if request.method == 'POST':
format_type = request.form['format']
password = request.form.get('password', '')
if format_type == 'pkcs12':
if not password:
flash('PKCS#12格式需要密码', 'danger')
return redirect(url_for('export_certificate_view', cert_id=cert_id))
pkcs12_path = export_pkcs12(cert_id, password)
if pkcs12_path:
return send_from_directory(
os.path.dirname(pkcs12_path),
os.path.basename(pkcs12_path),
as_attachment=True
)
else:
flash('导出PKCS#12失败', 'danger')
elif format_type == 'pem':
# 合并证书和私钥为PEM
pem_content = ""
with open(cert['cert_path'], 'r') as f:
pem_content += f.read()
with open(cert['key_path'], 'r') as f:
pem_content += f.read()
from io import StringIO
from flask import Response
return Response(
pem_content,
mimetype="application/x-pem-file",
headers={
"Content-Disposition": f"attachment; filename={cert['common_name']}.pem"
}
)
else:
flash('不支持的导出格式', 'danger')
return render_template('export_certificate.html', cert=cert)
@app.route('/download/<path:filename>')
@login_required
def download_file(filename):
# 安全检查:确保文件路径在证书存储目录内
file_path = os.path.join(CERT_STORE, filename)
if not os.path.exists(file_path):
flash('文件不存在', 'danger')
return redirect(url_for('index'))
# 检查权限
# 这里需要根据实际业务逻辑检查用户是否有权下载该文件
# 简单示例:只允许下载自己的证书文件
return send_from_directory(
os.path.dirname(file_path),
os.path.basename(file_path),
as_attachment=True
)
if __name__ == '__main__':
app.run(debug=True, ssl_context='adhoc', host='0.0.0.0', port='9875')

8
config.py Normal file
View File

@ -0,0 +1,8 @@
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here'
SQLALCHEMY_DATABASE_URI = 'sqlite:///cert_manager.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
CERTS_ROOT = os.path.join(os.path.dirname(__file__), 'certs')
CAPTCHA_ENABLED = True

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
Flask==2.0.1
Flask-Login==0.5.0
mysql-connector-python==8.0.26
python-dotenv==0.19.0

55
templates/base.html Normal file
View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>证书管理系统 - {% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">证书管理系统</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ca_list') }}">CA机构</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('certificate_list') }}">证书</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<span class="navbar-text me-3">欢迎, {{ current_user.username }}</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">退出</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

124
templates/ca_detail.html Normal file
View File

@ -0,0 +1,124 @@
{% extends "base.html" %}
{% block title %}{{ ca.name }} - CA详情{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>CA机构详情: {{ ca.name }}</h2>
<div>
<a href="{{ url_for('generate_crl_view', ca_id=ca.id) }}" class="btn btn-warning me-2">生成CRL</a>
{% if crl %}
<a href="{{ url_for('download_crl', ca_id=ca.id) }}" class="btn btn-success">下载CRL</a>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">基本信息</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">通用名</dt>
<dd class="col-sm-8">{{ ca.common_name }}</dd>
<dt class="col-sm-4">组织</dt>
<dd class="col-sm-8">{{ ca.organization }}</dd>
<dt class="col-sm-4">组织单位</dt>
<dd class="col-sm-8">{{ ca.organizational_unit or 'N/A' }}</dd>
<dt class="col-sm-4">国家</dt>
<dd class="col-sm-8">{{ ca.country }}</dd>
<dt class="col-sm-4">州/省</dt>
<dd class="col-sm-8">{{ ca.state or 'N/A' }}</dd>
<dt class="col-sm-4">城市</dt>
<dd class="col-sm-8">{{ ca.locality or 'N/A' }}</dd>
</dl>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">技术信息</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">密钥长度</dt>
<dd class="col-sm-8">{{ ca.key_size }}位</dd>
<dt class="col-sm-4">有效期</dt>
<dd class="col-sm-8">{{ ca.days_valid }}天</dd>
<dt class="col-sm-4">创建者</dt>
<dd class="col-sm-8">{{ ca.created_by }}</dd>
<dt class="col-sm-4">创建时间</dt>
<dd class="col-sm-8">{{ ca.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
<dt class="col-sm-4">证书路径</dt>
<dd class="col-sm-8"><code>{{ ca.cert_path }}</code></dd>
<dt class="col-sm-4">私钥路径</dt>
<dd class="col-sm-8"><code>{{ ca.key_path }}</code></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">颁发的证书</h5>
<a href="{{ url_for('create_certificate_view') }}?ca_id={{ ca.id }}" class="btn btn-sm btn-primary">创建证书</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>ID</th>
<th>通用名</th>
<th>状态</th>
<th>有效期至</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for cert in certificates %}
<tr>
<td>{{ cert.id }}</td>
<td>{{ cert.common_name }}</td>
<td>
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
{% else %}
<span class="badge bg-secondary">已过期</span>
{% endif %}
</td>
<td>{{ cert.expires_at.strftime('%Y-%m-%d') }}</td>
<td>{{ cert.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-sm btn-info">详情</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center">该CA尚未颁发任何证书</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

47
templates/ca_list.html Normal file
View File

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}CA机构列表{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>CA机构列表</h2>
<a href="{{ url_for('create_ca_view') }}" class="btn btn-primary">创建CA机构</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>名称</th>
<th>通用名</th>
<th>组织</th>
<th>有效期(天)</th>
<th>创建者</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for ca in cas %}
<tr>
<td>{{ ca.id }}</td>
<td>{{ ca.name }}</td>
<td>{{ ca.common_name }}</td>
<td>{{ ca.organization }}</td>
<td>{{ ca.days_valid }}</td>
<td>{{ ca.created_by }}</td>
<td>{{ ca.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}" class="btn btn-sm btn-info">详情</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center">暂无CA机构</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,148 @@
{% extends "base.html" %}
{% block title %}{{ cert.common_name }} - 证书详情{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>证书详情: {{ cert.common_name }}</h2>
<div>
{% if cert.status == 'active' %}
<a href="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}" class="btn btn-warning me-2">吊销证书</a>
{% endif %}
<a href="{{ url_for('renew_certificate_view', cert_id=cert.id) }}" class="btn btn-primary me-2">续期</a>
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}" class="btn btn-success">导出</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">基本信息</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">通用名</dt>
<dd class="col-sm-8">{{ cert.common_name }}</dd>
<dt class="col-sm-4">颁发CA</dt>
<dd class="col-sm-8">{{ ca.name }}</dd>
<dt class="col-sm-4">组织</dt>
<dd class="col-sm-8">{{ cert.organization }}</dd>
<dt class="col-sm-4">组织单位</dt>
<dd class="col-sm-8">{{ cert.organizational_unit or 'N/A' }}</dd>
<dt class="col-sm-4">国家</dt>
<dd class="col-sm-8">{{ cert.country }}</dd>
<dt class="col-sm-4">州/省</dt>
<dd class="col-sm-8">{{ cert.state or 'N/A' }}</dd>
<dt class="col-sm-4">城市</dt>
<dd class="col-sm-8">{{ cert.locality or 'N/A' }}</dd>
</dl>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">技术信息</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">密钥长度</dt>
<dd class="col-sm-8">{{ cert.key_size }}位</dd>
<dt class="col-sm-4">有效期</dt>
<dd class="col-sm-8">{{ cert.days_valid }}天</dd>
<dt class="col-sm-4">状态</dt>
<dd class="col-sm-8">
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
{% else %}
<span class="badge bg-secondary">已过期</span>
{% endif %}
</dd>
{% if cert.status == 'revoked' %}
<dt class="col-sm-4">吊销原因</dt>
<dd class="col-sm-8">{{ cert.revocation_reason or '未指定' }}</dd>
<dt class="col-sm-4">吊销时间</dt>
<dd class="col-sm-8">{{ cert.revoked_at.strftime('%Y-%m-%d %H:%M') }}</dd>
{% endif %}
<dt class="col-sm-4">创建者</dt>
<dd class="col-sm-8">{{ cert.created_by }}</dd>
<dt class="col-sm-4">创建时间</dt>
<dd class="col-sm-8">{{ cert.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
<dt class="col-sm-4">过期时间</dt>
<dd class="col-sm-8">{{ cert.expires_at.strftime('%Y-%m-%d %H:%M') }}</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">SAN扩展</h5>
</div>
<div class="card-body">
{% if cert.san_dns or cert.san_ip %}
{% if cert.san_dns %}
<h6>DNS名称:</h6>
<ul>
{% for dns in cert.san_dns.split(',') %}
<li>{{ dns }}</li>
{% endfor %}
</ul>
{% endif %}
{% if cert.san_ip %}
<h6>IP地址:</h6>
<ul>
{% for ip in cert.san_ip.split(',') %}
<li>{{ ip }}</li>
{% endfor %}
</ul>
{% endif %}
{% else %}
<p>未配置SAN扩展</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">文件路径</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">证书文件</dt>
<dd class="col-sm-8"><code>{{ cert.cert_path }}</code></dd>
<dt class="col-sm-4">私钥文件</dt>
<dd class="col-sm-8"><code>{{ cert.key_path }}</code></dd>
<dt class="col-sm-4">CSR文件</dt>
<dd class="col-sm-8"><code>{{ cert.csr_path }}</code></dd>
</dl>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block title %}证书列表{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>证书列表</h2>
<a href="{{ url_for('create_certificate_view') }}" class="btn btn-primary">创建证书</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>通用名</th>
<th>CA机构</th>
<th>状态</th>
<th>有效期至</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for cert in certificates %}
<tr>
<td>{{ cert.id }}</td>
<td>{{ cert.common_name }}</td>
<td>{{ cert.ca_name }}</td>
<td>
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
{% else %}
<span class="badge bg-secondary">已过期</span>
{% endif %}
</td>
<td>{{ cert.expires_at.strftime('%Y-%m-%d') }}</td>
<td>{{ cert.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-info">详情</a>
{% if cert.status == 'active' %}
<a href="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}" class="btn btn-warning">吊销</a>
{% endif %}
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}" class="btn btn-success">导出</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center">暂无证书</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

63
templates/create_ca.html Normal file
View File

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}创建CA机构{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4 class="card-title">创建新的CA机构</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('create_ca_view') }}">
<div class="row mb-3">
<div class="col-md-6">
<label for="ca_name" class="form-label">CA名称</label>
<input type="text" class="form-control" id="ca_name" name="ca_name" required>
<div class="form-text">CA机构的显示名称</div>
</div>
<div class="col-md-6">
<label for="common_name" class="form-label">通用名(CN)</label>
<input type="text" class="form-control" id="common_name" name="common_name" required>
<div class="form-text">证书的Common Name字段</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="organization" class="form-label">组织(O)</label>
<input type="text" class="form-control" id="organization" name="organization" required>
</div>
<div class="col-md-6">
<label for="organizational_unit" class="form-label">组织单位(OU)</label>
<input type="text" class="form-control" id="organizational_unit" name="organizational_unit">
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<label for="country" class="form-label">国家代码(C)</label>
<input type="text" class="form-control" id="country" name="country" maxlength="2" required>
<div class="form-text">2字母国家代码如CN</div>
</div>
<div class="col-md-3">
<label for="state" class="form-label">州/省(ST)</label>
<input type="text" class="form-control" id="state" name="state">
</div>
<div class="col-md-3">
<label for="locality" class="form-label">城市(L)</label>
<input type="text" class="form-control" id="locality" name="locality">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="key_size" class="form-label">密钥长度</label>
<select class="form-select" id="key_size" name="key_size">
<option value="2048">2048位</option>
<option value="3072">3072位</option>
<option value="4096">4096位</option>
</select>
</div>
<div class="col-md-6">
<label for="days_valid" class="form-label">有效期(天)</label>
<input type="number" class="form-control" id="days_valid" name="days_valid" value="3650"

View File

@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block title %}创建证书{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4 class="card-title">创建新证书</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('create_certificate_view') }}">
<div class="row mb-3">
<div class="col-md-6">
<label for="common_name" class="form-label">通用名(CN)</label>
<input type="text" class="form-control" id="common_name" name="common_name" required>
<div class="form-text">证书的Common Name字段通常是域名</div>
</div>
<div class="col-md-6">
<label for="ca_id" class="form-label">颁发CA</label>
<select class="form-select" id="ca_id" name="ca_id" required>
<option value="">-- 选择CA机构 --</option>
{% for ca in cas %}
<option value="{{ ca.id }}" {% if request.args.get('ca_id') == ca.id|string %}selected{% endif %}>{{ ca.name }} ({{ ca.common_name }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="organization" class="form-label">组织(O)</label>
<input type="text" class="form-control" id="organization" name="organization" required>
</div>
<div class="col-md-6">
<label for="organizational_unit" class="form-label">组织单位(OU)</label>
<input type="text" class="form-control" id="organizational_unit" name="organizational_unit">
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<label for="country" class="form-label">国家代码(C)</label>
<input type="text" class="form-control" id="country" name="country" maxlength="2" required>
<div class="form-text">2字母国家代码如CN</div>
</div>
<div class="col-md-3">
<label for="state" class="form-label">州/省(ST)</label>
<input type="text" class="form-control" id="state" name="state">
</div>
<div class="col-md-3">
<label for="locality" class="form-label">城市(L)</label>
<input type="text" class="form-control" id="locality" name="locality">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="san_dns" class="form-label">SAN DNS (可选)</label>
<input type="text" class="form-control" id="san_dns" name="san_dns">
<div class="form-text">多个DNS用逗号分隔如: example.com,www.example.com</div>
</div>
<div class="col-md-6">
<label for="san_ip" class="form-label">SAN IP (可选)</label>
<input type="text" class="form-control" id="san_ip" name="san_ip">
<div class="form-text">多个IP用逗号分隔如: 192.168.1.1,10.0.0.1</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="key_size" class="form-label">密钥长度</label>
<select class="form-select" id="key_size" name="key_size">
<option value="2048">2048位</option>
<option value="3072">3072位</option>
<option value="4096">4096位</option>
</select>
</div>
<div class="col-md-6">
<label for="days_valid" class="form-label">有效期(天)</label>
<input type="number" class="form-control" id="days_valid" name="days_valid" value="365" required>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('certificate_list') }}" class="btn btn-secondary me-md-2">取消</a>
<button type="submit" class="btn btn-primary">创建证书</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}导出证书 - {{ cert.common_name }}{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4 class="card-title">导出证书: {{ cert.common_name }}</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('export_certificate_view', cert_id=cert.id) }}">
<div class="mb-3">
<label class="form-label">导出格式</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="formatPkcs12" value="pkcs12" checked>
<label class="form-check-label" for="formatPkcs12">
PKCS#12 (.p12/.pfx)
</label>
<div class="form-text">包含证书和私钥的加密存档,适用于大多数应用</div>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="formatPem" value="pem">
<label class="form-check-label" for="formatPem">
PEM (.pem)
</label>
<div class="form-text">Base64编码的证书和私钥适用于Nginx/Apache等</div>
</div>
</div>
<div class="mb-3" id="passwordField">
<label for="password" class="form-label">PKCS#12密码</label>
<input type="password" class="form-control" id="password" name="password">
<div class="form-text">用于保护PKCS#12文件的密码</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-secondary me-md-2">取消</a>
<button type="submit" class="btn btn-primary">导出证书</button>
</div>
</form>
</div>
</div>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const formatRadios = document.querySelectorAll('input[name="format"]');
const passwordField = document.getElementById('passwordField');
function togglePasswordField() {
const selectedFormat = document.querySelector('input[name="format"]:checked').value;
passwordField.style.display = selectedFormat === 'pkcs12' ? 'block' : 'none';
}
formatRadios.forEach(radio => {
radio.addEventListener('change', togglePasswordField);
});
// 初始化状态
togglePasswordField();
});
</script>
{% endblock %}
{% endblock %}

40
templates/index.html Normal file
View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}首页{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">系统概览</h5>
</div>
<div class="card-body">
<p>欢迎使用证书管理系统!</p>
<div class="row">
<div class="col-md-6">
<div class="card bg-light mb-3">
<div class="card-body">
<h5 class="card-title">CA机构</h5>
<p class="card-text">
<a href="{{ url_for('ca_list') }}">查看所有CA机构</a>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-light mb-3">
<div class="card-body">
<h5 class="card-title">证书</h5>
<p class="card-text">
<a href="{{ url_for('certificate_list') }}">查看所有证书</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

32
templates/login.html Normal file
View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}登录{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">用户登录</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="captcha" class="form-label">验证码: <strong>{{ captcha_code }}</strong></label>
<input type="text" class="form-control" id="captcha" name="captcha" required>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

43
templates/register.html Normal file
View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}用户注册{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">用户注册</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('register') }}">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
<div class="form-text">请输入4-20位的字母、数字或下划线</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
<div class="form-text">至少8位字符包含字母和数字</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">确认密码</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">邮箱(可选)</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="captcha" class="form-label">验证码: <strong>{{ captcha_code }}</strong></label>
<input type="text" class="form-control" id="captcha" name="captcha" required>
</div>
<button type="submit" class="btn btn-primary">注册</button>
<a href="{{ url_for('login') }}" class="btn btn-link">已有账号?去登录</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block title %}续期证书 - {{ cert.common_name }}{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4 class="card-title">续期证书: {{ cert.common_name }}</h4>
</div>
<div class="card-body">
<div class="alert alert-info">
<p><strong>当前有效期:</strong> {{ cert.expires_at.strftime('%Y-%m-%d') }}</p>
<p>续期将使用相同的CSR和密钥对生成新证书。</p>
</div>
<form method="POST" action="{{ url_for('renew_certificate_view', cert_id=cert.id) }}">
<div class="mb-3">
<label for="days_valid" class="form-label">新的有效期(天)</label>
<input type="number" class="form-control" id="days_valid" name="days_valid" value="365" required>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-secondary me-md-2">取消</a>
<button type="submit" class="btn btn-primary">确认续期</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}吊销证书 - {{ cert.common_name }}{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4 class="card-title">吊销证书: {{ cert.common_name }}</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<strong>警告!</strong> 此操作不可逆。吊销后证书将立即失效。
</div>
<form method="POST" action="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}">
<div class="mb-3">
<label for="reason" class="form-label">吊销原因</label>
<select class="form-select" id="reason" name="reason" required>
<option value="">-- 选择吊销原因 --</option>
<option value="unspecified">未指定</option>
<option value="keyCompromise">密钥泄露</option>
<option value="CACompromise">CA泄露</option>
<option value="affiliationChanged">隶属关系变更</option>
<option value="superseded">被取代</option>
<option value="cessationOfOperation">停止运营</option>
<option value="certificateHold">证书暂停</option>
<option value="removeFromCRL">从CRL移除</option>
</select>
</div>
<div class="mb-3">
<label for="details" class="form-label">详细说明 (可选)</label>
<textarea class="form-control" id="details" name="details" rows="3"></textarea>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-secondary me-md-2">取消</a>
<button type="submit" class="btn btn-danger">确认吊销</button>
</div>
</form>
</div>
</div>
{% endblock %}