first commit
This commit is contained in:
commit
37610aeb2c
5
.env
Normal file
5
.env
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
8
.idea/certmgr.iml
generated
Normal 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
28
.idea/deployment.xml
generated
Normal 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>
|
||||
16
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
16
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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>
|
||||
BIN
__pycache__/config.cpython-38.pyc
Normal file
BIN
__pycache__/config.cpython-38.pyc
Normal file
Binary file not shown.
951
app.py
Normal file
951
app.py
Normal 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
8
config.py
Normal 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
4
requirements.txt
Normal 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
55
templates/base.html
Normal 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
124
templates/ca_detail.html
Normal 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
47
templates/ca_list.html
Normal 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 %}
|
||||
148
templates/certificate_detail.html
Normal file
148
templates/certificate_detail.html
Normal 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 %}
|
||||
59
templates/certificate_list.html
Normal file
59
templates/certificate_list.html
Normal 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
63
templates/create_ca.html
Normal 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"
|
||||
91
templates/create_certificate.html
Normal file
91
templates/create_certificate.html
Normal 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 %}
|
||||
64
templates/export_certificate.html
Normal file
64
templates/export_certificate.html
Normal 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
40
templates/index.html
Normal 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
32
templates/login.html
Normal 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
43
templates/register.html
Normal 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 %}
|
||||
29
templates/renew_certificate.html
Normal file
29
templates/renew_certificate.html
Normal 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 %}
|
||||
43
templates/revoke_certificate.html
Normal file
43
templates/revoke_certificate.html
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user