Compare commits

..

43 Commits
ca ... main

Author SHA1 Message Date
p6s4t3fer 348f90079f ADD file via upload
2 weeks ago
p6s4t3fer e298ad4457 Delete 'README.md'
2 weeks ago
p6s4t3fer 55c48da444 ADD file via upload
2 weeks ago
p6s4t3fer 0bd68c072b ADD file via upload
2 weeks ago
p6s4t3fer e0ac3fd9ac ADD file via upload
2 weeks ago
p6s4t3fer bd9b9a0d3c ADD file via upload
2 weeks ago
p6s4t3fer 4f595feaed ADD file via upload
2 weeks ago
p6s4t3fer b8bc3e312a Delete 'README.md'
2 weeks ago
p6s4t3fer 7c6f48df8c ADD file via upload
2 weeks ago
p6s4t3fer 568b8debeb ADD file via upload
2 weeks ago
p6s4t3fer 5996bb70e8 ADD file via upload
2 weeks ago
p6s4t3fer 284bf49c06 ADD file via upload
2 weeks ago
p6s4t3fer ca93fd9e7a ADD file via upload
2 weeks ago
p6s4t3fer 5db5ecbe03 ADD file via upload
2 weeks ago
p6s4t3fer e7e465e84a ADD file via upload
2 weeks ago
p6s4t3fer 79b2ed2dcd ADD file via upload
2 weeks ago
p6s4t3fer 14e7b15dc1 ADD file via upload
2 weeks ago
p6s4t3fer 32bae8e31b ADD file via upload
2 weeks ago
p6s4t3fer d6a96c2955 ADD file via upload
2 weeks ago
p6s4t3fer 7f58619a47 ADD file via upload
2 weeks ago
p6s4t3fer cfaaaddff9 Delete 'frontend'
2 weeks ago
p6s4t3fer 31e23f6b3d ADD file via upload
2 weeks ago
p6s4t3fer f6c3d6dfb2 Delete 'CA'
2 weeks ago
p6s4t3fer 24d8d9fb97 ADD file via upload
2 weeks ago
p6s4t3fer 01bf40d419 ADD file via upload
2 weeks ago
p6s4t3fer ff40590ba8 ADD file via upload
2 weeks ago
p6s4t3fer cd84ff1918 ADD file via upload
2 weeks ago
p6s4t3fer 0af6c2351b ADD file via upload
2 weeks ago
p6s4t3fer 71ba45ba30 ADD file via upload
2 weeks ago
p6s4t3fer 34cd91fe70 ADD file via upload
2 weeks ago
p6s4t3fer 7e1d889284 ADD file via upload
2 weeks ago
p6s4t3fer bb5f871bc5 ADD file via upload
2 weeks ago
p6s4t3fer d31820a7ef ADD file via upload
2 weeks ago
p6s4t3fer c34f43ed9d ADD file via upload
2 weeks ago
p6s4t3fer d61f3fb5e7 ADD file via upload
2 weeks ago
p6s4t3fer 9262ef9ec6 ADD file via upload
2 weeks ago
p6s4t3fer 5e0dd1245c ADD file via upload
2 weeks ago
p6s4t3fer bdeaf1032a ADD file via upload
2 weeks ago
p6s4t3fer bb8e6899aa ADD file via upload
2 weeks ago
p6s4t3fer 46691b5af5 ADD file via upload
2 weeks ago
p6s4t3fer 44536ce315 ADD file via upload
2 weeks ago
p6s4t3fer fd2c693ac4 Delete 'README.md'
2 weeks ago
p6s4t3fer fc894a3858 Delete 'app.py'
2 weeks ago

@ -0,0 +1,33 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
min-height: 100vh;
background: #f5f5f5;
}
</style>

@ -0,0 +1,216 @@
<template>
<div class="admin-certificates-page">
<el-card>
<template #header>
<div class="card-header">
<span>证书管理</span>
<el-button type="primary" @click="loadCertificates"></el-button>
</div>
</template>
<el-table :data="certificates" v-loading="loading" style="width: 100%">
<el-table-column prop="id" label="证书ID" width="100" />
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="serial_number" label="序列号" width="180" />
<el-table-column prop="state_text" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.state === 1 ? 'success' : 'danger'">
{{ row.state_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="expire_time" label="过期时间" width="180">
<template #default="{ row }">
{{ formatDate(row.expire_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)"></el-button>
<el-button
v-if="row.state === 1"
size="small"
type="danger"
@click="handleRevoke(row)"
>
吊销
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 证书详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="证书详情" width="80%">
<div v-if="certificateDetail" class="cert-detail" v-loading="detailLoading">
<el-tabs>
<el-tab-pane label="基本信息">
<el-descriptions :column="2" border>
<el-descriptions-item label="证书ID">{{ certificateDetail.id }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ certificateDetail.username || '-' }}</el-descriptions-item>
<el-descriptions-item label="序列号">{{ certificateDetail.serial_number }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="certificateDetail.state === 1 ? 'success' : 'danger'">
{{ certificateDetail.state_text }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(certificateDetail.created_at) }}</el-descriptions-item>
<el-descriptions-item label="过期时间">{{ formatDate(certificateDetail.expire_time) }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="证书信息" v-if="certificateDetail.certificate_info">
<el-descriptions :column="2" border>
<el-descriptions-item label="国家" v-if="certificateDetail.certificate_info.subject.country">
{{ certificateDetail.certificate_info.subject.country }}
</el-descriptions-item>
<el-descriptions-item label="省/市" v-if="certificateDetail.certificate_info.subject.province">
{{ certificateDetail.certificate_info.subject.province }}
</el-descriptions-item>
<el-descriptions-item label="地区" v-if="certificateDetail.certificate_info.subject.locality">
{{ certificateDetail.certificate_info.subject.locality }}
</el-descriptions-item>
<el-descriptions-item label="组织" v-if="certificateDetail.certificate_info.subject.organization">
{{ certificateDetail.certificate_info.subject.organization }}
</el-descriptions-item>
<el-descriptions-item label="部门" v-if="certificateDetail.certificate_info.subject.organization_unit_name">
{{ certificateDetail.certificate_info.subject.organization_unit_name }}
</el-descriptions-item>
<el-descriptions-item label="域名">
{{ certificateDetail.certificate_info.subject.common_name }}
</el-descriptions-item>
<el-descriptions-item label="邮箱" v-if="certificateDetail.certificate_info.subject.email_address">
{{ certificateDetail.certificate_info.subject.email_address }}
</el-descriptions-item>
<el-descriptions-item label="颁发者">
{{ certificateDetail.certificate_info.issuer.common_name || certificateDetail.certificate_info.issuer.organization }}
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="证书内容" v-if="certificateDetail.certificate_content">
<el-input
type="textarea"
:value="certificateDetail.certificate_content"
:rows="15"
readonly
/>
</el-tab-pane>
</el-tabs>
</div>
</el-dialog>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { getAllCertificates, getCertificateDetail, revokeCertificate } from '@/api/admin'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
name: 'AdminCertificates',
setup() {
const certificates = ref([])
const loading = ref(false)
const detailLoading = ref(false)
const detailDialogVisible = ref(false)
const certificateDetail = ref(null)
const loadCertificates = async () => {
loading.value = true
try {
const res = await getAllCertificates()
certificates.value = res.data
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
const viewDetail = async (cert) => {
detailLoading.value = true
detailDialogVisible.value = true
try {
const res = await getCertificateDetail(cert.id)
certificateDetail.value = res.data
} catch (error) {
console.error(error)
// 使
certificateDetail.value = cert
} finally {
detailLoading.value = false
}
}
const handleRevoke = (cert) => {
ElMessageBox.confirm('确定要吊销此证书吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await revokeCertificate(cert.id)
ElMessage.success('证书已吊销')
loadCertificates()
} catch (error) {
console.error(error)
}
}).catch(() => {})
}
const formatDate = (dateString) => {
if (!dateString) return '-'
// +08:00
const date = new Date(dateString)
// 使toLocaleStringAsia/Shanghai
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
}
onMounted(() => {
loadCertificates()
})
return {
certificates,
loading,
detailLoading,
detailDialogVisible,
certificateDetail,
loadCertificates,
viewDetail,
handleRevoke,
formatDate
}
}
}
</script>
<style scoped>
.admin-certificates-page {
max-width: 1400px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.cert-detail {
padding: 20px 0;
}
.cert-info-section {
margin-top: 20px;
}
</style>

@ -0,0 +1,162 @@
<template>
<div class="login-container">
<div class="login-box">
<h2 class="title">小型CA系统</h2>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-form" @submit.prevent="handleLogin">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
size="large"
clearable
:prefix-icon="User"
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
size="large"
clearable
show-password
:prefix-icon="Lock"
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
class="login-button"
>
登录
</el-button>
</el-form-item>
<el-form-item>
<div class="register-link">
<span>还没有账号</span>
<router-link to="/register">立即注册</router-link>
</div>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { login } from '@/api/auth'
import { setToken, setUserInfo } from '@/utils/auth'
import { ElMessage } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
export default {
name: 'Login',
setup() {
const router = useRouter()
const loginFormRef = ref(null)
const loginForm = reactive({
username: '',
password: ''
})
const loading = ref(false)
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: ['blur', 'change'] }
],
password: [
{ required: true, message: '请输入密码', trigger: ['blur', 'change'] }
]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
//
try {
await loginFormRef.value.validate()
} catch (error) {
return false
}
loading.value = true
try {
const res = await login(loginForm)
setToken(res.data.token)
setUserInfo(res.data.user)
ElMessage.success('登录成功')
router.push('/')
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
return {
loginFormRef,
loginForm,
loading,
rules,
handleLogin,
User,
Lock
}
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box {
width: 400px;
padding: 40px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.title {
text-align: center;
margin-bottom: 30px;
color: #333;
font-size: 28px;
}
.login-form {
margin-top: 20px;
}
.login-button {
width: 100%;
}
.register-link {
text-align: center;
width: 100%;
color: #666;
}
.register-link a {
color: #409eff;
text-decoration: none;
margin-left: 5px;
}
.register-link a:hover {
text-decoration: underline;
}
</style>

@ -0,0 +1,157 @@
<template>
<div class="main-layout">
<el-container>
<el-header class="header">
<div class="header-left">
<h1 class="logo">小型CA系统</h1>
<el-menu
:default-active="activeMenu"
mode="horizontal"
router
class="nav-menu"
>
<el-menu-item index="/certificates">我的证书</el-menu-item>
<el-menu-item index="/certificate/register">注册证书</el-menu-item>
<el-menu-item index="/certificate/verify">验证证书</el-menu-item>
<el-sub-menu v-if="isAdmin" index="/admin">
<template #title>管理后台</template>
<el-menu-item index="/admin/requests">证书审核</el-menu-item>
<el-menu-item index="/admin/certificates">证书管理</el-menu-item>
</el-sub-menu>
</el-menu>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<span class="user-info">
<el-icon><User /></el-icon>
{{ userInfo?.username }}
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { logout, getCurrentUser } from '@/api/auth'
import { removeToken, getUserInfo, setUserInfo } from '@/utils/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
import { User, ArrowDown } from '@element-plus/icons-vue'
export default {
name: 'MainLayout',
setup() {
const router = useRouter()
const route = useRoute()
const userInfo = ref(getUserInfo())
const isAdmin = computed(() => userInfo.value?.authority === 1)
const activeMenu = computed(() => route.path)
onMounted(async () => {
try {
const res = await getCurrentUser()
setUserInfo(res.data)
userInfo.value = res.data
} catch (error) {
console.error(error)
}
})
const handleCommand = async (command) => {
if (command === 'logout') {
try {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await logout()
} catch (error) {
console.error(error)
}
removeToken()
ElMessage.success('已退出登录')
router.push('/login')
})
} catch (error) {
//
}
}
}
return {
userInfo,
isAdmin,
activeMenu,
handleCommand,
User,
ArrowDown
}
}
}
</script>
<style scoped>
.main-layout {
min-height: 100vh;
}
.header {
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.header-left {
display: flex;
align-items: center;
flex: 1;
}
.logo {
margin: 0;
margin-right: 30px;
font-size: 20px;
color: #409eff;
}
.nav-menu {
border-bottom: none;
}
.header-right {
display: flex;
align-items: center;
}
.user-info {
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
color: #333;
}
.main-content {
padding: 20px;
background: #f5f5f5;
min-height: calc(100vh - 60px);
}
</style>

@ -0,0 +1,168 @@
# 故障排除指南
## 常见问题
### 1. 前端无法连接后端 (ECONNREFUSED)
**错误信息**: `Error: connect ECONNREFUSED ::1:5000`
**原因**:
- 后端服务没有运行
- IPv6/IPv4地址解析问题
**解决方案**:
1. **确保后端服务正在运行**:
```bash
cd backend
python app.py
```
应该看到类似输出:
```
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://[::]:5000
```
2. **测试后端是否正常**:
在浏览器访问: `http://127.0.0.1:5000/`
应该看到 JSON 响应:
```json
{
"status": "ok",
"message": "CA System API is running",
"version": "1.0.0"
}
```
3. **重启前端开发服务器**:
```bash
cd frontend
# 按 Ctrl+C 停止
npm run dev
```
### 2. 注册时返回500错误
**检查步骤**:
1. **查看后端控制台日志**:
后端会打印详细的错误信息,包括:
- SQL 查询语句
- Python 异常堆栈
2. **检查数据库连接**:
```bash
cd backend
python test_db.py
```
3. **确保数据库表已创建**:
```bash
mysql -u root -p Simple_CA < init_database.sql
```
4. **检查数据库配置**:
确认 `backend/config.py` 中的配置正确:
```python
MYSQL_HOST = '127.0.0.1'
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'sxd123' # 您的密码
MYSQL_DATABASE = 'Simple_CA'
```
### 3. 数据库连接失败
**错误信息**: `(2003, "Can't connect to MySQL server")`
**解决方案**:
1. **检查MySQL服务是否运行**:
- Windows: 打开"服务"管理器查找MySQL服务
- Linux/Mac: `sudo systemctl status mysql``brew services list`
2. **检查数据库是否存在**:
```sql
mysql -u root -p
SHOW DATABASES;
```
如果没有 `Simple_CA` 数据库,创建它:
```sql
CREATE DATABASE Simple_CA DEFAULT CHARACTER SET = 'utf8mb4';
```
3. **检查用户权限**:
确保root用户有足够权限访问数据库
### 4. 前端页面空白或组件未加载
**解决方案**:
1. **清除缓存并重新安装依赖**:
```bash
cd frontend
rm -rf node_modules
npm install
```
2. **检查浏览器控制台**:
按F12打开开发者工具查看Console标签是否有错误
3. **检查端口占用**:
```bash
# Windows
netstat -ano | findstr :8080
# Linux/Mac
lsof -i :8080
```
### 5. 图标不显示
**解决方案**:
确保安装了图标包:
```bash
cd frontend
npm install @element-plus/icons-vue
```
## 调试技巧
### 查看后端日志
后端已启用详细日志SQL查询所有错误都会打印到控制台。
### 查看网络请求
1. 打开浏览器开发者工具 (F12)
2. 切换到 Network 标签
3. 尝试注册操作
4. 查看请求详情:
- Request URL
- Request Headers
- Response Status
- Response Body
### 测试API接口
使用测试脚本:
```bash
cd backend
python test_register.py
```
## 快速检查清单
- [ ] MySQL 服务正在运行
- [ ] 数据库 `Simple_CA` 已创建
- [ ] 数据库表已创建(执行了 init_database.sql
- [ ] 后端服务正在运行 (`python app.py`)
- [ ] 可以访问 `http://127.0.0.1:5000/`
- [ ] 前端服务正在运行 (`npm run dev`)
- [ ] 前端可以访问 `http://localhost:8080`
- [ ] 浏览器控制台没有错误

@ -0,0 +1,203 @@
<template>
<div class="register-container">
<div class="register-box">
<h2 class="title">用户注册</h2>
<el-form ref="registerFormRef" :model="registerForm" :rules="rules" class="register-form" @submit.prevent="handleRegister">
<el-form-item prop="username">
<el-input
v-model="registerForm.username"
placeholder="用户名不超过16个字符"
size="large"
clearable
:prefix-icon="User"
@keyup.enter="handleRegister"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="密码"
size="large"
clearable
show-password
:prefix-icon="Lock"
/>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="确认密码"
size="large"
clearable
show-password
:prefix-icon="Lock"
@keyup.enter="handleRegister"
/>
</el-form-item>
<el-form-item prop="email">
<el-input
v-model="registerForm.email"
placeholder="邮箱(可选)"
size="large"
clearable
:prefix-icon="Message"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleRegister"
class="register-button"
>
注册
</el-button>
</el-form-item>
<el-form-item>
<div class="login-link">
<span>已有账号</span>
<router-link to="/login">立即登录</router-link>
</div>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { register } from '@/api/auth'
import { ElMessage } from 'element-plus'
import { User, Lock, Message } from '@element-plus/icons-vue'
export default {
name: 'Register',
setup() {
const router = useRouter()
const registerFormRef = ref(null)
const registerForm = reactive({
username: '',
password: '',
confirmPassword: '',
email: ''
})
const loading = ref(false)
const validateConfirmPassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请确认密码'))
} else if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: ['blur', 'change'] },
{ max: 16, message: '用户名长度不能超过16个字符', trigger: ['blur', 'change'] }
],
password: [
{ required: true, message: '请输入密码', trigger: ['blur', 'change'] },
{ min: 6, message: '密码长度不能少于6个字符', trigger: ['blur', 'change'] }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: ['blur', 'change'] },
{ validator: validateConfirmPassword, trigger: ['blur', 'change'] }
]
}
const handleRegister = async () => {
if (!registerFormRef.value) return
//
try {
await registerFormRef.value.validate()
} catch (error) {
return false
}
loading.value = true
try {
await register({
username: registerForm.username,
password: registerForm.password,
email: registerForm.email || null
})
ElMessage.success('注册成功,请登录')
router.push('/login')
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
return {
registerFormRef,
registerForm,
loading,
rules,
handleRegister,
User,
Lock,
Message
}
}
}
</script>
<style scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.register-box {
width: 400px;
padding: 40px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.title {
text-align: center;
margin-bottom: 30px;
color: #333;
font-size: 28px;
}
.register-form {
margin-top: 20px;
}
.register-button {
width: 100%;
}
.login-link {
text-align: center;
width: 100%;
color: #666;
}
.login-link a {
color: #409eff;
text-decoration: none;
margin-left: 5px;
}
.login-link a:hover {
text-decoration: underline;
}
</style>

@ -0,0 +1,376 @@
<template>
<div class="register-certificate-page">
<el-card>
<template #header>
<span>注册CA证书</span>
</template>
<el-steps :active="currentStep" finish-status="success" align-center>
<el-step title="基本信息" />
<el-step title="公钥提交" />
<el-step title="完成" />
</el-steps>
<!-- 第一步基本信息 -->
<div v-if="currentStep === 0" class="step-content">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-form-item label="国家" prop="country">
<el-input v-model="form.country" placeholder="例如CN必须是2个字符的国家代码如CN、US" maxlength="2" />
<div class="tip">请输入ISO 3166-1标准的2字符国家代码CN(中国)US(美国)GB(英国)</div>
</el-form-item>
<el-form-item label="省/市" prop="province">
<el-input v-model="form.province" placeholder="例如Beijing" />
</el-form-item>
<el-form-item label="地区" prop="locality">
<el-input v-model="form.locality" placeholder="例如Beijing" />
</el-form-item>
<el-form-item label="公司/组织" prop="organization">
<el-input v-model="form.organization" placeholder="例如My Company" />
</el-form-item>
<el-form-item label="部门" prop="organization_unit_name">
<el-input v-model="form.organization_unit_name" placeholder="例如IT Department" />
</el-form-item>
<el-form-item label="域名" prop="common_name">
<el-input v-model="form.common_name" placeholder="必填例如example.com" />
</el-form-item>
<el-form-item label="电子邮件" prop="email_address">
<el-input v-model="form.email_address" placeholder="例如admin@example.com" />
</el-form-item>
<el-form-item label="上传CSR文件">
<el-upload
:auto-upload="false"
:on-change="handleCSRChange"
:file-list="csrFileList"
accept=".csr"
:limit="1"
>
<el-button type="primary">选择CSR文件</el-button>
<template #tip>
<div class="el-upload__tip">可拖拽或点击上传.csr文件可选</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitBasicInfo"></el-button>
</el-form-item>
</el-form>
</div>
<!-- 第二步公钥提交 -->
<div v-if="currentStep === 1" class="step-content">
<el-form label-width="120px">
<el-form-item label="自动生成密钥">
<el-button type="primary" @click="handleGenerateKeyPair" :loading="generating">
自动生成RSA密钥对
</el-button>
</el-form-item>
<el-form-item label="私钥PEM格式" v-if="keyPair.private_key">
<el-input
type="textarea"
v-model="keyPair.private_key"
:rows="8"
placeholder="私钥内容"
/>
<div class="tip">请妥善保存私钥系统不会存储私钥</div>
</el-form-item>
<el-form-item label="公钥PEM格式" v-if="keyPair.public_key">
<el-input
type="textarea"
v-model="keyPair.public_key"
:rows="8"
placeholder="公钥内容"
/>
</el-form-item>
<el-form-item label="或手动提交公钥">
<el-input
type="textarea"
v-model="form.public_key"
:rows="8"
placeholder="请输入RSA公钥PEM格式"
/>
</el-form-item>
<el-form-item>
<el-button @click="currentStep = 0">上一步</el-button>
<el-button type="primary" @click="submitKey" :loading="submitting">
提交并等待审核
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 第三步完成 -->
<div v-if="currentStep === 2" class="step-content">
<el-result
icon="success"
title="申请已提交"
sub-title="您的证书申请已提交,请等待管理员审核"
>
<template #extra>
<el-button type="primary" @click="$router.push('/certificates')"></el-button>
<el-button @click="resetForm"></el-button>
</template>
</el-result>
</div>
</el-card>
<!-- 申请状态 -->
<el-card style="margin-top: 20px">
<template #header>
<span>申请状态</span>
</template>
<el-table :data="requestStatus" v-loading="statusLoading">
<el-table-column prop="id" label="申请ID" width="100" />
<el-table-column prop="common_name" label="域名" />
<el-table-column prop="state_text" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStateType(row.state)">
{{ row.state_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="申请时间" width="180">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { requestCertificate, uploadCSR, submitKey as submitKeyApi, generateKeyPair, getRequestStatus } from '@/api/certificate'
import { ElMessage } from 'element-plus'
export default {
name: 'RegisterCertificate',
setup() {
const currentStep = ref(0)
const formRef = ref(null)
const generating = ref(false)
const submitting = ref(false)
const statusLoading = ref(false)
const csrFileList = ref([])
const requestStatus = ref([])
const form = reactive({
country: '',
province: '',
locality: '',
organization: '',
organization_unit_name: '',
common_name: '',
email_address: '',
public_key: '',
request_id: null
})
const keyPair = reactive({
private_key: '',
public_key: ''
})
const rules = {
common_name: [
{ required: true, message: '域名不能为空', trigger: 'blur' }
],
country: [
{
validator: (rule, value, callback) => {
if (value && value.length !== 2) {
callback(new Error('国家代码必须是2个字符如CN、US'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
const handleCSRChange = (file) => {
const reader = new FileReader()
reader.onload = async (e) => {
try {
const csrContent = e.target.result
const fileObj = new File([csrContent], file.name, { type: 'text/plain' })
const res = await uploadCSR(fileObj)
if (res.code === 200) {
ElMessage.success('CSR文件上传成功')
//
if (res.data.parsed_data) {
const data = res.data.parsed_data
form.country = data.country || ''
form.province = data.province || ''
form.locality = data.locality || ''
form.organization = data.organization || ''
form.organization_unit_name = data.organization_unit_name || ''
form.common_name = data.common_name || ''
form.email_address = data.email_address || ''
form.request_id = res.data.request_id
currentStep.value = 1
}
}
} catch (error) {
console.error(error)
}
}
reader.readAsText(file.raw)
}
const submitBasicInfo = async () => {
if (!formRef.value) return
formRef.value.validate(async (valid) => {
if (!valid) return
try {
const res = await requestCertificate(form)
if (res.code === 200) {
form.request_id = res.data.request_id
currentStep.value = 1
}
} catch (error) {
console.error(error)
}
})
}
const handleGenerateKeyPair = async () => {
generating.value = true
try {
const res = await generateKeyPair()
keyPair.private_key = res.data.private_key
keyPair.public_key = res.data.public_key
form.public_key = res.data.public_key
ElMessage.success('密钥对生成成功,请保存私钥')
} catch (error) {
console.error(error)
} finally {
generating.value = false
}
}
const submitKey = async () => {
if (!form.public_key && !keyPair.public_key) {
ElMessage.warning('请提供公钥或生成密钥对')
return
}
if (submitting.value) {
return //
}
submitting.value = true
try {
await submitKeyApi({
request_id: form.request_id,
public_key: form.public_key || keyPair.public_key,
private_key: keyPair.private_key || undefined
})
ElMessage.success('公钥提交成功,等待审核')
currentStep.value = 2
//
setTimeout(() => {
loadRequestStatus()
}, 500)
} catch (error) {
console.error(error)
} finally {
submitting.value = false
}
}
const resetForm = () => {
currentStep.value = 0
Object.keys(form).forEach(key => {
if (key !== 'request_id') {
form[key] = ''
}
})
keyPair.private_key = ''
keyPair.public_key = ''
csrFileList.value = []
}
const loadRequestStatus = async () => {
//
if (statusLoading.value) {
return
}
statusLoading.value = true
try {
const res = await getRequestStatus()
requestStatus.value = res.data || []
} catch (error) {
console.error(error)
} finally {
statusLoading.value = false
}
}
const getStateType = (state) => {
const types = { 1: 'warning', 2: 'success', 3: 'danger' }
return types[state] || ''
}
const formatDate = (dateString) => {
if (!dateString) return '-'
// +08:00
const date = new Date(dateString)
// 使toLocaleStringAsia/Shanghai
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
}
onMounted(() => {
loadRequestStatus()
})
return {
currentStep,
formRef,
form,
keyPair,
rules,
generating,
submitting,
statusLoading,
csrFileList,
requestStatus,
handleCSRChange,
submitBasicInfo,
handleGenerateKeyPair,
submitKey,
resetForm,
getStateType,
formatDate
}
}
}
</script>
<style scoped>
.register-certificate-page {
max-width: 900px;
margin: 0 auto;
}
.step-content {
margin-top: 40px;
padding: 20px;
}
.tip {
margin-top: 5px;
color: #909399;
font-size: 12px;
}
</style>

@ -0,0 +1,239 @@
<template>
<div class="admin-requests-page">
<el-card>
<template #header>
<div class="card-header">
<span>证书审核</span>
<el-radio-group v-model="filterState" @change="loadRequests">
<el-radio-button :label="1">待审核</el-radio-button>
<el-radio-button :label="2">已通过</el-radio-button>
<el-radio-button :label="3">已拒绝</el-radio-button>
</el-radio-group>
</div>
</template>
<el-table :data="requests" v-loading="loading" style="width: 100%">
<el-table-column prop="id" label="申请ID" width="100" />
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="common_name" label="域名" />
<el-table-column prop="organization" label="组织" />
<el-table-column prop="email_address" label="邮箱" />
<el-table-column prop="state_text" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStateType(row.state)">
{{ row.state_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="申请时间" width="180">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)"></el-button>
<template v-if="filterState === 1">
<el-button size="small" type="success" @click="handleApprove(row)">
同意
</el-button>
<el-button size="small" type="danger" @click="handleReject(row)">
拒绝
</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="申请详情" width="80%">
<div v-if="selectedRequest" class="request-detail" v-loading="detailLoading">
<el-tabs>
<el-tab-pane label="申请信息">
<el-descriptions :column="2" border>
<el-descriptions-item label="申请ID">{{ selectedRequest.id }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ selectedRequest.username || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStateType(selectedRequest.state)">
{{ selectedRequest.state_text }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ formatDate(selectedRequest.created_at) }}</el-descriptions-item>
<el-descriptions-item label="国家">{{ selectedRequest.country || '-' }}</el-descriptions-item>
<el-descriptions-item label="省/市">{{ selectedRequest.province || '-' }}</el-descriptions-item>
<el-descriptions-item label="地区">{{ selectedRequest.locality || '-' }}</el-descriptions-item>
<el-descriptions-item label="组织">{{ selectedRequest.organization || '-' }}</el-descriptions-item>
<el-descriptions-item label="部门">{{ selectedRequest.organization_unit_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="域名">{{ selectedRequest.common_name }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ selectedRequest.email_address || '-' }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 证书信息如果已签发 -->
<el-tab-pane label="证书信息" v-if="selectedRequest.certificate">
<el-descriptions :column="2" border>
<el-descriptions-item label="证书ID">{{ selectedRequest.certificate.id }}</el-descriptions-item>
<el-descriptions-item label="序列号">{{ selectedRequest.certificate.serial_number }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="selectedRequest.certificate.state === 1 ? 'success' : 'danger'">
{{ selectedRequest.certificate.state_text }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(selectedRequest.certificate.created_at) }}</el-descriptions-item>
<el-descriptions-item label="过期时间">{{ formatDate(selectedRequest.certificate.expire_time) }}</el-descriptions-item>
</el-descriptions>
<div v-if="selectedRequest.certificate.certificate_info" class="cert-info-section" style="margin-top: 20px">
<h3>证书详细信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="国家" v-if="selectedRequest.certificate.certificate_info.subject.country">
{{ selectedRequest.certificate.certificate_info.subject.country }}
</el-descriptions-item>
<el-descriptions-item label="省/市" v-if="selectedRequest.certificate.certificate_info.subject.province">
{{ selectedRequest.certificate.certificate_info.subject.province }}
</el-descriptions-item>
<el-descriptions-item label="组织" v-if="selectedRequest.certificate.certificate_info.subject.organization">
{{ selectedRequest.certificate.certificate_info.subject.organization }}
</el-descriptions-item>
<el-descriptions-item label="域名">
{{ selectedRequest.certificate.certificate_info.subject.common_name }}
</el-descriptions-item>
<el-descriptions-item label="颁发者">
{{ selectedRequest.certificate.certificate_info.issuer.common_name || selectedRequest.certificate.certificate_info.issuer.organization }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-dialog>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { getPendingRequests, approveRequest, rejectRequest, getRequestDetail } from '@/api/admin'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
name: 'AdminRequests',
setup() {
const requests = ref([])
const loading = ref(false)
const detailLoading = ref(false)
const filterState = ref(1)
const detailDialogVisible = ref(false)
const selectedRequest = ref(null)
const loadRequests = async () => {
loading.value = true
try {
const res = await getPendingRequests(filterState.value)
requests.value = res.data
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
const handleApprove = (request) => {
ElMessageBox.confirm('确定要通过此证书申请吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await approveRequest(request.id)
ElMessage.success('申请已通过,证书已签发')
loadRequests()
} catch (error) {
console.error(error)
}
}).catch(() => {})
}
const handleReject = (request) => {
ElMessageBox.confirm('确定要拒绝此证书申请吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await rejectRequest(request.id)
ElMessage.success('申请已拒绝')
loadRequests()
} catch (error) {
console.error(error)
}
}).catch(() => {})
}
const viewDetail = async (request) => {
detailLoading.value = true
detailDialogVisible.value = true
try {
const res = await getRequestDetail(request.id)
selectedRequest.value = res.data
} catch (error) {
console.error(error)
// 使
selectedRequest.value = request
} finally {
detailLoading.value = false
}
}
const getStateType = (state) => {
const types = { 1: 'warning', 2: 'success', 3: 'danger' }
return types[state] || ''
}
const formatDate = (dateString) => {
if (!dateString) return '-'
// +08:00
const date = new Date(dateString)
// 使toLocaleStringAsia/Shanghai
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
}
onMounted(() => {
loadRequests()
})
return {
requests,
loading,
detailLoading,
filterState,
detailDialogVisible,
selectedRequest,
loadRequests,
handleApprove,
handleReject,
viewDetail,
getStateType,
formatDate
}
}
}
</script>
<style scoped>
.admin-requests-page {
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.request-detail {
padding: 20px 0;
}
</style>

@ -0,0 +1,85 @@
# 快速安装指南
## 前置要求
- Python 3.8+
- Node.js 16+
- MySQL 5.7+
## 步骤
### 1. 数据库设置
```bash
# 登录MySQL
mysql -u root -p
# 执行数据库创建脚本您提供的SQL
# 然后执行补充SQL
mysql -u root -p Simple_CA < database_supplement.sql
```
### 2. 后端设置
```bash
cd backend
# 创建虚拟环境(推荐)
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
# 创建管理员账号
python init_admin.py
# 启动后端服务
python app.py
```
后端将在 `http://localhost:5000` 运行
### 3. 前端设置
```bash
cd frontend
# 安装依赖
npm install
# 启动开发服务器
npm run dev
```
前端将在 `http://localhost:8080` 运行
## 首次使用
1. 访问 `http://localhost:8080`
2. 使用管理员账号登录(通过 `init_admin.py` 创建)
3. 普通用户可以注册新账号
## 常见问题
### 数据库连接失败
- 检查 `backend/config.py` 中的数据库配置
- 确保MySQL服务已启动
- 检查用户名和密码是否正确
### CA证书初始化失败
- 确保 `backend/ca/` 目录有写权限
- 检查是否有足够的磁盘空间
### 前端无法连接后端
- 检查后端服务是否运行在 5000 端口
- 检查 `frontend/vite.config.js` 中的代理配置

@ -0,0 +1,166 @@
<template>
<div class="verify-certificate-page">
<el-card>
<template #header>
<span>验证CA证书</span>
</template>
<div class="upload-area">
<el-upload
class="upload-dragger"
drag
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
accept=".cer"
:limit="1"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将证书文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">只能上传.cer证书文件</div>
</template>
</el-upload>
</div>
<div class="verify-button">
<el-button type="primary" size="large" @click="handleVerify" :loading="verifying" :disabled="!selectedFile">
验证证书
</el-button>
</div>
<!-- 验证结果 -->
<div v-if="verifyResult" class="verify-result">
<el-card>
<el-result
:icon="verifyResult.is_valid ? 'success' : 'error'"
:title="verifyResult.is_valid ? '证书有效' : '证书无效'"
:sub-title="verifyResult.message"
>
<template #extra v-if="verifyResult.is_valid && verifyResult.certificate_info">
<div class="cert-info">
<el-descriptions :column="2" border title="证书信息">
<el-descriptions-item label="序列号">
{{ verifyResult.certificate_info.serial_number }}
</el-descriptions-item>
<el-descriptions-item label="域名">
{{ verifyResult.certificate_info.subject.common_name }}
</el-descriptions-item>
<el-descriptions-item label="组织" v-if="verifyResult.certificate_info.subject.organization">
{{ verifyResult.certificate_info.subject.organization }}
</el-descriptions-item>
<el-descriptions-item label="邮箱" v-if="verifyResult.certificate_info.subject.email_address">
{{ verifyResult.certificate_info.subject.email_address }}
</el-descriptions-item>
<el-descriptions-item label="生效时间">
{{ formatDate(verifyResult.certificate_info.not_valid_before) }}
</el-descriptions-item>
<el-descriptions-item label="过期时间">
{{ formatDate(verifyResult.certificate_info.not_valid_after) }}
</el-descriptions-item>
<el-descriptions-item label="颁发者">
{{ verifyResult.certificate_info.issuer.common_name || verifyResult.certificate_info.issuer.organization }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
</el-result>
</el-card>
</div>
</el-card>
</div>
</template>
<script>
import { ref } from 'vue'
import { verifyCertificate } from '@/api/certificate'
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
export default {
name: 'VerifyCertificate',
components: {
UploadFilled
},
setup() {
const fileList = ref([])
const selectedFile = ref(null)
const verifying = ref(false)
const verifyResult = ref(null)
const handleFileChange = (file) => {
selectedFile.value = file.raw
fileList.value = [file]
}
const handleVerify = async () => {
if (!selectedFile.value) {
ElMessage.warning('请先选择证书文件')
return
}
verifying.value = true
try {
const res = await verifyCertificate(selectedFile.value)
verifyResult.value = res.data
} catch (error) {
console.error(error)
} finally {
verifying.value = false
}
}
const formatDate = (dateString) => {
if (!dateString) return '-'
// +08:00
const date = new Date(dateString)
// 使toLocaleStringAsia/Shanghai
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
}
return {
fileList,
selectedFile,
verifying,
verifyResult,
handleFileChange,
handleVerify,
formatDate
}
}
}
</script>
<style scoped>
.verify-certificate-page {
max-width: 900px;
margin: 0 auto;
}
.upload-area {
margin: 40px 0;
}
.upload-dragger {
width: 100%;
}
.verify-button {
text-align: center;
margin: 30px 0;
}
.verify-result {
margin-top: 30px;
}
.cert-info {
margin-top: 20px;
text-align: left;
}
</style>

@ -0,0 +1,52 @@
import request from '@/utils/request'
export function getPendingRequests(state = 1) {
return request({
url: '/admin/requests',
method: 'get',
params: { state }
})
}
export function approveRequest(requestId) {
return request({
url: `/admin/request/${requestId}/approve`,
method: 'post'
})
}
export function rejectRequest(requestId) {
return request({
url: `/admin/request/${requestId}/reject`,
method: 'post'
})
}
export function getAllCertificates() {
return request({
url: '/admin/certificates',
method: 'get'
})
}
export function revokeCertificate(certId) {
return request({
url: `/admin/certificate/${certId}/revoke`,
method: 'post'
})
}
export function getRequestDetail(requestId) {
return request({
url: `/admin/request/${requestId}`,
method: 'get'
})
}
export function getCertificateDetail(certId) {
return request({
url: `/admin/certificate/${certId}`,
method: 'get'
})
}

@ -0,0 +1,288 @@
from flask import Blueprint, request, jsonify
from models import CARequest, Certificate, CRL, User, db
from utils.cert_utils import sign_certificate, sign_certificate_from_request, parse_certificate
from middleware.auth_middleware import admin_required
from datetime import datetime, timezone
def get_beijing_now():
"""获取当前北京时间UTC+8用于数据库存储"""
from datetime import timezone, timedelta
beijing_tz = timezone(timedelta(hours=8))
utc_now = datetime.now(timezone.utc)
beijing_now = utc_now.astimezone(beijing_tz)
return beijing_now.replace(tzinfo=None)
admin_bp = Blueprint('admin', __name__)
@admin_bp.route('/requests', methods=['GET'])
@admin_required
def get_pending_requests():
"""获取待审核的证书请求"""
state = request.args.get('state', '1', type=int) # 默认为待审核
requests = CARequest.query.filter_by(
state=state,
deleted_at=None
).order_by(CARequest.created_at.desc()).all()
request_list = []
for req in requests:
req_dict = req.to_dict()
# 获取用户信息
user = User.query.get(req.user_id)
if user:
req_dict['username'] = user.username
# 检查是否已生成证书
cert = Certificate.query.filter_by(
request_id=req.id,
deleted_at=None
).first()
req_dict['has_certificate'] = cert is not None
if cert:
req_dict['certificate_id'] = cert.id
request_list.append(req_dict)
return jsonify({
'code': 200,
'data': request_list
}), 200
@admin_bp.route('/request/<int:request_id>', methods=['GET'])
@admin_required
def get_request_detail(request_id):
"""获取证书请求详细信息(包括证书信息)"""
cert_request = CARequest.query.filter_by(
id=request_id,
deleted_at=None
).first()
if not cert_request:
return jsonify({'code': 404, 'message': '证书请求不存在'}), 404
req_dict = cert_request.to_dict()
# 获取用户信息
user = User.query.get(cert_request.user_id)
if user:
req_dict['username'] = user.username
# 获取证书信息(如果已签发)
cert = Certificate.query.filter_by(
request_id=request_id,
deleted_at=None
).first()
if cert:
req_dict['certificate'] = cert.to_dict()
# 解析证书内容
if cert.certificate_content:
try:
cert_info = parse_certificate(cert.certificate_content)
req_dict['certificate']['certificate_info'] = cert_info
except Exception as e:
print(f'解析证书信息失败: {e}')
return jsonify({
'code': 200,
'data': req_dict
}), 200
@admin_bp.route('/request/<int:request_id>/approve', methods=['POST'])
@admin_required
def approve_request(request_id):
"""同意证书申请"""
cert_request = CARequest.query.filter_by(
id=request_id,
deleted_at=None
).first()
if not cert_request:
return jsonify({'code': 404, 'message': '证书请求不存在'}), 404
if cert_request.state != 1:
return jsonify({'code': 400, 'message': '该请求已处理'}), 400
# 检查必要字段
if not cert_request.common_name:
return jsonify({'code': 400, 'message': '证书请求缺少域名(common_name)'}), 400
if not cert_request.public_key:
return jsonify({'code': 400, 'message': '证书请求缺少公钥'}), 400
try:
# 如果有CSR内容使用CSR签署证书
if cert_request.csr_content:
cert_pem, serial_number, expire_time = sign_certificate(
cert_request.csr_content,
request_id
)
else:
# 如果没有CSR根据请求信息直接生成证书
cert_pem, serial_number, expire_time = sign_certificate_from_request(cert_request)
# 创建证书记录
# expire_time 是UTC时间从证书中获取需要转换为北京时间存储
from datetime import timezone, timedelta
beijing_tz = timezone(timedelta(hours=8))
if expire_time.tzinfo is None:
# 如果是naive datetime假设是UTC时间
expire_time_utc = expire_time.replace(tzinfo=timezone.utc)
else:
expire_time_utc = expire_time.astimezone(timezone.utc)
# 转换为北京时间并去掉时区信息因为数据库存储naive datetime
expire_time_beijing = expire_time_utc.astimezone(beijing_tz).replace(tzinfo=None)
certificate = Certificate(
user_id=cert_request.user_id,
state=1, # 使用中
request_id=request_id,
expire_time=expire_time_beijing, # 存储为北京时间
certificate_content=cert_pem,
serial_number=serial_number
)
db.session.add(certificate)
# 更新请求状态
cert_request.state = 2 # 审核通过
cert_request.updated_at = get_beijing_now()
db.session.commit()
return jsonify({
'code': 200,
'message': '证书已签发',
'data': {
'certificate_id': certificate.id,
'serial_number': serial_number
}
}), 200
except Exception as e:
db.session.rollback()
import traceback
traceback.print_exc()
return jsonify({'code': 500, 'message': f'签发证书失败: {str(e)}'}), 500
@admin_bp.route('/request/<int:request_id>/reject', methods=['POST'])
@admin_required
def reject_request(request_id):
"""拒绝证书申请"""
cert_request = CARequest.query.filter_by(
id=request_id,
deleted_at=None
).first()
if not cert_request:
return jsonify({'code': 404, 'message': '证书请求不存在'}), 404
if cert_request.state != 1:
return jsonify({'code': 400, 'message': '该请求已处理'}), 400
# 更新请求状态
cert_request.state = 3 # 审核未通过
cert_request.updated_at = get_beijing_now()
db.session.commit()
return jsonify({
'code': 200,
'message': '申请已拒绝'
}), 200
@admin_bp.route('/certificates', methods=['GET'])
@admin_required
def get_all_certificates():
"""获取所有证书"""
certificates = Certificate.query.filter_by(
deleted_at=None
).order_by(Certificate.created_at.desc()).all()
cert_list = []
for cert in certificates:
cert_dict = cert.to_dict()
# 获取用户信息
user = User.query.get(cert.user_id)
if user:
cert_dict['username'] = user.username
# 检查是否已过期(证书过期时间存储在数据库中,是北京时间)
# 如果证书在使用中但已过期,更新状态
if cert.expire_time and cert.expire_time < get_beijing_now() and cert.state == 1:
cert.state = 2 # 更新数据库状态为已过期
cert_dict['state'] = 2
cert_dict['state_text'] = '已过期'
db.session.commit()
cert_list.append(cert_dict)
return jsonify({
'code': 200,
'data': cert_list
}), 200
@admin_bp.route('/certificate/<int:cert_id>', methods=['GET'])
@admin_required
def get_certificate_detail(cert_id):
"""获取证书详细信息"""
cert = Certificate.query.filter_by(
id=cert_id,
deleted_at=None
).first()
if not cert:
return jsonify({'code': 404, 'message': '证书不存在'}), 404
cert_dict = cert.to_dict()
# 获取用户信息
user = User.query.get(cert.user_id)
if user:
cert_dict['username'] = user.username
# 解析证书内容
if cert.certificate_content:
try:
cert_info = parse_certificate(cert.certificate_content)
cert_dict['certificate_info'] = cert_info
cert_dict['certificate_content'] = cert.certificate_content
except Exception as e:
print(f'解析证书信息失败: {e}')
return jsonify({
'code': 200,
'data': cert_dict
}), 200
@admin_bp.route('/certificate/<int:cert_id>/revoke', methods=['POST'])
@admin_required
def revoke_certificate(cert_id):
"""管理员吊销证书"""
cert = Certificate.query.filter_by(
id=cert_id,
deleted_at=None
).first()
if not cert:
return jsonify({'code': 404, 'message': '证书不存在'}), 404
if cert.state == 2:
return jsonify({'code': 400, 'message': '证书已被吊销'}), 400
# 更新证书状态
cert.state = 2
cert.updated_at = get_beijing_now()
# 添加到CRL
from datetime import timezone
crl = CRL(
certificate_id=cert_id,
input_time=int(datetime.now(timezone.utc).timestamp()) # CRL时间戳使用UTC
)
db.session.add(crl)
db.session.commit()
return jsonify({
'code': 200,
'message': '证书已吊销'
}), 200

@ -0,0 +1,40 @@
import request from '@/utils/request'
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
export function register(data) {
return request({
url: '/auth/register',
method: 'post',
data
})
}
export function logout() {
return request({
url: '/auth/logout',
method: 'post'
})
}
export function getCurrentUser() {
return request({
url: '/auth/me',
method: 'get'
})
}

@ -0,0 +1,144 @@
from flask import Blueprint, request, jsonify
from models import User, db
from utils.auth_utils import hash_password, verify_password, generate_token, revoke_token
from middleware.auth_middleware import login_required
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/register', methods=['POST'])
def register():
"""用户注册"""
try:
data = request.get_json()
if not data:
return jsonify({'code': 400, 'message': '请求数据不能为空'}), 400
username = data.get('username')
password = data.get('password')
email = data.get('email')
if not username or not password:
return jsonify({'code': 400, 'message': '用户名和密码不能为空'}), 400
if len(username) > 16:
return jsonify({'code': 400, 'message': '用户名长度不能超过16个字符'}), 400
# 检查用户名是否已存在
existing_user = User.query.filter_by(username=username, deleted_at=None).first()
if existing_user:
return jsonify({'code': 400, 'message': '用户名已存在'}), 400
# 创建新用户
hashed_password = hash_password(password)
new_user = User(
username=username,
password=hashed_password,
email=email if email else None,
authority=0
)
db.session.add(new_user)
# 刷新以获取ID
db.session.flush()
user_id = new_user.id
# 提交事务
db.session.commit()
# 验证用户是否真的保存成功
saved_user = User.query.get(user_id)
if not saved_user:
raise Exception('用户保存失败')
return jsonify({
'code': 200,
'message': '注册成功',
'data': new_user.to_dict()
}), 200
except Exception as e:
db.session.rollback()
import traceback
error_msg = str(e)
traceback.print_exc()
print(f'注册失败错误详情: {error_msg}')
return jsonify({
'code': 500,
'message': f'注册失败: {error_msg}'
}), 500
@auth_bp.route('/login', methods=['POST'])
def login():
"""用户登录"""
try:
data = request.get_json()
if not data:
return jsonify({'code': 400, 'message': '请求数据不能为空'}), 400
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'code': 400, 'message': '用户名和密码不能为空'}), 400
# 查找用户
user = User.query.filter_by(username=username, deleted_at=None).first()
if not user:
return jsonify({'code': 401, 'message': '用户名或密码错误'}), 401
# 验证密码
if not verify_password(password, user.password):
return jsonify({'code': 401, 'message': '用户名或密码错误'}), 401
# 生成Token
token, expire_time = generate_token(user.id, user.username)
return jsonify({
'code': 200,
'message': '登录成功',
'data': {
'user': user.to_dict(),
'token': token,
'expire_time': expire_time
}
}), 200
except Exception as e:
import traceback
error_msg = str(e)
traceback.print_exc()
print(f'登录失败错误详情: {error_msg}')
return jsonify({
'code': 500,
'message': f'登录失败: {error_msg}'
}), 500
@auth_bp.route('/logout', methods=['POST'])
@login_required
def logout():
"""用户退出登录"""
token = request.headers.get('Authorization')
if token.startswith('Bearer '):
token = token[7:]
revoke_token(token)
return jsonify({
'code': 200,
'message': '退出登录成功'
}), 200
@auth_bp.route('/me', methods=['GET'])
@login_required
def get_current_user():
"""获取当前用户信息"""
user = User.query.get(request.user_id)
if not user:
return jsonify({'code': 404, 'message': '用户不存在'}), 404
return jsonify({
'code': 200,
'data': user.to_dict()
}), 200

@ -0,0 +1,50 @@
from functools import wraps
from flask import request, jsonify
from utils.auth_utils import verify_token
from models import User
def login_required(f):
"""登录验证装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'code': 401, 'message': '未提供Token'}), 401
# 移除Bearer前缀如果有
if token.startswith('Bearer '):
token = token[7:]
payload = verify_token(token)
if not payload:
return jsonify({'code': 401, 'message': 'Token无效或已过期'}), 401
# 将用户信息添加到request中
request.user_id = payload['user_id']
request.username = payload['username']
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""管理员权限验证装饰器"""
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
user = User.query.get(request.user_id)
if not user or user.authority != 1:
return jsonify({'code': 403, 'message': '需要管理员权限'}), 403
return f(*args, **kwargs)
return decorated_function

@ -0,0 +1,92 @@
import bcrypt
import time
from datetime import datetime, timedelta, timezone
from config import Config
from models import UserToken, db
def get_beijing_now():
"""获取当前北京时间UTC+8用于数据库存储"""
from datetime import timezone, timedelta
beijing_tz = timezone(timedelta(hours=8))
utc_now = datetime.now(timezone.utc)
beijing_now = utc_now.astimezone(beijing_tz)
return beijing_now.replace(tzinfo=None)
# 使用 PyJWT确保安装的是 PyJWT 而不是其他 jwt 包)
# 如果导入失败或不是正确的模块,会给出明确的错误提示
try:
import jwt
# 验证是否是PyJWT检查是否有encode和decode方法
if not hasattr(jwt, 'encode') or not hasattr(jwt, 'decode'):
raise ImportError(
"导入的jwt模块不正确。请卸载错误的jwt包并安装PyJWT:\n"
" pip uninstall jwt\n"
" pip install PyJWT"
)
except ImportError as e:
raise ImportError(
f"无法导入PyJWT模块: {e}\n"
"请安装 PyJWT: pip install PyJWT"
)
def hash_password(password):
"""加密密码"""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def verify_password(password, hashed):
"""验证密码"""
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
def generate_token(user_id, username):
"""生成JWT Token"""
expire_time = int(time.time()) + int(Config.JWT_EXPIRATION_DELTA.total_seconds())
payload = {
'user_id': user_id,
'username': username,
'exp': expire_time
}
token = jwt.encode(payload, Config.SECRET_KEY, algorithm='HS256')
# 保存Token到数据库
user_token = UserToken(
user_id=user_id,
token=token,
expire_time=expire_time
)
db.session.add(user_token)
db.session.commit()
return token, expire_time
def verify_token(token):
"""验证Token"""
try:
payload = jwt.decode(token, Config.SECRET_KEY, algorithms=['HS256'])
user_id = payload.get('user_id')
# 检查Token是否在数据库中且未过期
user_token = UserToken.query.filter_by(
token=token,
user_id=user_id,
deleted_at=None
).first()
if not user_token:
return None
if user_token.expire_time < int(time.time()):
return None
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def revoke_token(token):
"""撤销Token"""
user_token = UserToken.query.filter_by(token=token, deleted_at=None).first()
if user_token:
user_token.deleted_at = get_beijing_now()
db.session.commit()

@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIURTnwQt2PO0mUtEFOychDPgWdxDUwDQYJKoZIhvcNAQEL
BQAwZTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl
aWppbmcxGTAXBgNVBAoMEFNpbXBsZSBDQSBTeXN0ZW0xFzAVBgNVBAMMDlNpbXBs
ZSBDQSBSb290MB4XDTI1MTIyOTAyMDcyMloXDTM1MTIyNzAyMDcyMlowZTELMAkG
A1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0JlaWppbmcxGTAX
BgNVBAoMEFNpbXBsZSBDQSBTeXN0ZW0xFzAVBgNVBAMMDlNpbXBsZSBDQSBSb290
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArlep+Euflf0vY95Ibi6H
bOnHh7DJGUZ0R0LCiDM5EXQ7nYuGSynZeHCh3H5wjirZaGivOkI6AByCyK4u7a9T
FFBTfehfFaVFPHM8+TlNpUgS04nyYuPq0fB/oeVDGMNWRRpdB68Odp0IWQ4BqFIS
Hk5hSadrV5udaKwjrEBfazTwwvDWDlkonkGsMjuCwBBiIOLXHPXEs7KRBx4SR8Ck
FOZMwabBviF7T5IWTPEK29ARMK89wgxWr1YlbDwGk7wrre2lbZHr8+QicK2W2WrZ
uhNVnWLEEt4u7cufhGthw6bACr3qVWOlsIgDa35xYLz7b9s5ndZbbgkwaQBKUvJW
kQIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAV
jhvTC9T8KhDnCXpecNkBzvrKFMiWv04niVV9NYtyjgbOElszEl9tVqQA7QgdLwDv
9PNE1oRslktLQ3/J/4tuGBWuA6ZJ37nfYCJrCw2EFO9hwUEqq30d2R0enFDdeaLq
tnOGKa5UOrR6IHuwt3OFH4hJR+179rAers4e42psT266vNaYYoC/BapyZ0gqSTnE
5iL7hjoBbZGlagHx7E7DVFn5JKogxBpWp4+bxBQI+ztPG/58a76a+fcCwusMoX37
TOLrzWhFY/51Hp/wp4r9RdyQcIm86gCg3x/OBFioUaA5D45TswHBWfIAa8QLffvS
P/nOLL6ZPR2SuOTYorMC
-----END CERTIFICATE-----

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuV6n4S5+V/S9j
3khuLods6ceHsMkZRnRHQsKIMzkRdDudi4ZLKdl4cKHcfnCOKtloaK86QjoAHILI
ri7tr1MUUFN96F8VpUU8czz5OU2lSBLTifJi4+rR8H+h5UMYw1ZFGl0Hrw52nQhZ
DgGoUhIeTmFJp2tXm51orCOsQF9rNPDC8NYOWSieQawyO4LAEGIg4tcc9cSzspEH
HhJHwKQU5kzBpsG+IXtPkhZM8Qrb0BEwrz3CDFavViVsPAaTvCut7aVtkevz5CJw
rZbZatm6E1WdYsQS3i7ty5+Ea2HDpsAKvepVY6WwiANrfnFgvPtv2zmd1ltuCTBp
AEpS8laRAgMBAAECggEAFBptsgDeXQg937Ew/uuEmC144Y+kELMME2+CSPxHF1kk
yqzdBmvD9Nxf/bHivrH4Mc7obbpXP84J0qQrKyMtXElK51jVJgTRr//FxyMxbd6a
tSPR/E81s5Gc1gk+rGtR1lQM6Cbqbwj6fnJcBJG6Hx6An2KbwRVjmD9JOcKOfikC
3kW8mHf/qXzvWeO/Vt5n2QL5MAaUh7wAI+PPY6ifw7A6+EhlR5JqKuNrOsR329Q+
d6Q3NpWu90moWZxhL7e708b4gvZumLBvJ3/U5Jts1VGMvhVy2jCGVjcP8tkKo6ON
SIHmsPdu+/QYk/nc/W3IVBs6eF5Yu51+/PS9dP+iBQKBgQDai/liPMt75SG3sBqj
mklhFO3AovLaLpptXmg2uIIPVwzYryZcBSoOBv+ckiA9Ku9Qy+sjMoCqHy8af13v
Zc8ojzV/4fOAWJoDY6CZE3BZSD4+ZxKrqRvutbL9F5ahBEUPPr+i4dxZm/FINB0s
jgDRRLzowEZarWIHvRTeQKDlkwKBgQDMOF5whpDjczf6gqIZsrWrU5xk3Yvs2L/u
Rjwh1gtqA3briPyrPKcDrp7NZZgboRaaqOayKXsoag4N9woaKDzFe4MfAnUSpPoc
nToZxhpuiHLQjzsG2AMSStDp6AELyyFLkt9PUKHS3ANrxmOS1FW7d0ClgEZJHNr3
8Vcq1eFpywKBgBOAWgiCFEZB8/hIJphitBmNnImMAM+nSPBpdDMt0606v1K2jl23
uED4Lubxwx6yLhivmZPSddi4X6OlqeQq2Yls/gEjUG8reNLEmRgPu/67i5JV8DyR
IoTygb5D1JUZpG/v0XnfXaJBYA5pWBEZusjxsUmznpOI1S6LmMkcSz8xAoGBAJOt
r51vshbImxJspQwwT281Z0MJcsYzd5e28oUFY/ulxblNtmmd0qi2/d9KOFTCiLYO
rcToekcfakpu+r8vmcK1LnyeKkrxQmyuxfUdbL/BrEVlTioEyzPPJoP/YcJFgi5E
Z3fTtzHkKBUnwZnMMrL96aHEwQ7d7vqda4tuXzNBAoGBALfV9wWtbLWtCaLzbrSG
zs/GuZxmzWHD7KewImNmj5nk/zQCEclycXaSLn+ETcVzJmRDEJxdB87dnebe3t0f
0ThiBRRTbqIJ/onlyaruJiKIONBmJI+ERsD7wTTYlQMY4rlhRtPQCFA3zlPwqtQv
ea4KNFTyTXx0CJzS8fbY8WXD
-----END PRIVATE KEY-----

@ -0,0 +1,443 @@
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from datetime import datetime, timedelta, timezone
import os
from backend.config import Config
def get_utc_now():
"""获取当前UTC时间确保时间同步"""
# 返回naive UTC时间因为cryptography库的证书时间字段需要naive datetime
# 使用now(timezone.utc)确保获取的是UTC时间不受系统时区影响
return datetime.now(timezone.utc).replace(tzinfo=None)
def generate_key_pair():
"""生成RSA密钥对"""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=Config.CA_KEY_SIZE,
backend=default_backend()
)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
public_key = private_key.public_key()
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return private_pem.decode('utf-8'), public_pem.decode('utf-8')
def init_ca():
"""初始化CA根证书如果不存在"""
if os.path.exists(Config.CA_PRIVATE_KEY_PATH) and os.path.exists(Config.CA_CERTIFICATE_PATH):
return
# 生成CA密钥对
ca_private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=Config.CA_KEY_SIZE,
backend=default_backend()
)
# 创建CA证书
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Beijing"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Beijing"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Simple CA System"),
x509.NameAttribute(NameOID.COMMON_NAME, "Simple CA Root"),
])
now = get_utc_now()
ca_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
ca_private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
now
).not_valid_after(
now + timedelta(days=3650) # 10年有效期
).add_extension(
x509.BasicConstraints(ca=True, path_length=None), critical=True,
).sign(ca_private_key, hashes.SHA256(), default_backend())
# 保存CA私钥
ca_private_pem = ca_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
# 保存CA证书
ca_cert_pem = ca_cert.public_bytes(serialization.Encoding.PEM)
# 确保目录存在
os.makedirs('ca', exist_ok=True)
with open(Config.CA_PRIVATE_KEY_PATH, 'wb') as f:
f.write(ca_private_pem)
with open(Config.CA_CERTIFICATE_PATH, 'wb') as f:
f.write(ca_cert_pem)
def load_ca_private_key():
"""加载CA私钥"""
with open(Config.CA_PRIVATE_KEY_PATH, 'rb') as f:
return serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
def load_ca_certificate():
"""加载CA证书"""
with open(Config.CA_CERTIFICATE_PATH, 'rb') as f:
return x509.load_pem_x509_certificate(f.read(), default_backend())
def validate_country_code(country):
"""验证并转换国家代码为2个字符"""
if not country:
return None
country = country.strip().upper()
# 如果是2个字符直接返回
if len(country) == 2:
return country
# 常见国家名称到代码的映射
country_map = {
'CHINA': 'CN',
'USA': 'US',
'UNITED STATES': 'US',
'UNITED KINGDOM': 'GB',
'UK': 'GB',
'JAPAN': 'JP',
'GERMANY': 'DE',
'FRANCE': 'FR',
'CANADA': 'CA',
'AUSTRALIA': 'AU',
'SOUTH KOREA': 'KR',
'KOREA': 'KR',
'INDIA': 'IN',
'BRAZIL': 'BR',
'RUSSIA': 'RU',
'ITALY': 'IT',
'SPAIN': 'ES',
'NETHERLANDS': 'NL',
'SWEDEN': 'SE',
'NORWAY': 'NO',
'DENMARK': 'DK',
'FINLAND': 'FI',
'POLAND': 'PL',
'SWITZERLAND': 'CH',
'AUSTRIA': 'AT',
'BELGIUM': 'BE',
'IRELAND': 'IE',
'PORTUGAL': 'PT',
'GREECE': 'GR',
'TURKEY': 'TR',
'MEXICO': 'MX',
'ARGENTINA': 'AR',
'SOUTH AFRICA': 'ZA',
'SINGAPORE': 'SG',
'HONG KONG': 'HK',
'TAIWAN': 'TW',
'THAILAND': 'TH',
'VIETNAM': 'VN',
'INDONESIA': 'ID',
'MALAYSIA': 'MY',
'PHILIPPINES': 'PH',
'NEW ZEALAND': 'NZ'
}
# 尝试映射
if country in country_map:
return country_map[country]
# 如果长度超过2取前2个字符可能不准确但至少能通过验证
if len(country) > 2:
raise ValueError(f'国家代码必须是2个字符如CN、US当前输入: {country}。请输入ISO 3166-1 alpha-2标准的2字符国家代码')
return country
def create_csr_from_data(data):
"""从数据创建CSR"""
private_key = serialization.load_pem_private_key(
data['private_key'].encode('utf-8'),
password=None,
backend=default_backend()
)
name_attributes = []
if data.get('country'):
# 验证并转换国家代码
country_code = validate_country_code(data['country'])
if country_code:
name_attributes.append(x509.NameAttribute(NameOID.COUNTRY_NAME, country_code))
if data.get('province'):
name_attributes.append(x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, data['province']))
if data.get('locality'):
name_attributes.append(x509.NameAttribute(NameOID.LOCALITY_NAME, data['locality']))
if data.get('organization'):
name_attributes.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, data['organization']))
if data.get('organization_unit_name'):
name_attributes.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, data['organization_unit_name']))
name_attributes.append(x509.NameAttribute(NameOID.COMMON_NAME, data['common_name']))
if data.get('email_address'):
name_attributes.append(x509.NameAttribute(NameOID.EMAIL_ADDRESS, data['email_address']))
builder = x509.CertificateSigningRequestBuilder()
builder = builder.subject_name(x509.Name(name_attributes))
csr = builder.sign(private_key, hashes.SHA256(), default_backend())
return csr, private_key
def parse_csr(csr_pem):
"""解析CSR文件"""
csr = x509.load_pem_x509_csr(csr_pem.encode('utf-8'), default_backend())
data = {}
for attr in csr.subject:
if attr.oid == NameOID.COUNTRY_NAME:
data['country'] = attr.value
elif attr.oid == NameOID.STATE_OR_PROVINCE_NAME:
data['province'] = attr.value
elif attr.oid == NameOID.LOCALITY_NAME:
data['locality'] = attr.value
elif attr.oid == NameOID.ORGANIZATION_NAME:
data['organization'] = attr.value
elif attr.oid == NameOID.ORGANIZATIONAL_UNIT_NAME:
data['organization_unit_name'] = attr.value
elif attr.oid == NameOID.COMMON_NAME:
data['common_name'] = attr.value
elif attr.oid == NameOID.EMAIL_ADDRESS:
data['email_address'] = attr.value
public_key = csr.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
return data, public_key
def sign_certificate_from_request(cert_request):
"""根据证书请求直接生成证书不需要CSR"""
ca_private_key = load_ca_private_key()
ca_cert = load_ca_certificate()
# 构建主题名称
name_attributes = []
if cert_request.country:
# 验证并转换国家代码
country_code = validate_country_code(cert_request.country)
if country_code:
name_attributes.append(x509.NameAttribute(NameOID.COUNTRY_NAME, country_code))
if cert_request.province:
name_attributes.append(x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, cert_request.province))
if cert_request.locality:
name_attributes.append(x509.NameAttribute(NameOID.LOCALITY_NAME, cert_request.locality))
if cert_request.organization:
name_attributes.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, cert_request.organization))
if cert_request.organization_unit_name:
name_attributes.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, cert_request.organization_unit_name))
name_attributes.append(x509.NameAttribute(NameOID.COMMON_NAME, cert_request.common_name))
if cert_request.email_address:
name_attributes.append(x509.NameAttribute(NameOID.EMAIL_ADDRESS, cert_request.email_address))
subject_name = x509.Name(name_attributes)
# 加载公钥
# 尝试加载PEM格式的公钥
public_key_data = cert_request.public_key.encode('utf-8')
try:
public_key = serialization.load_pem_public_key(public_key_data, backend=default_backend())
except Exception:
# 如果不是标准PEM格式尝试添加PEM头尾
if not public_key_data.startswith(b'-----BEGIN'):
# 尝试添加RSA公钥头尾
if b'BEGIN PUBLIC KEY' not in public_key_data:
public_key_data = b'-----BEGIN PUBLIC KEY-----\n' + public_key_data + b'\n-----END PUBLIC KEY-----'
public_key = serialization.load_pem_public_key(public_key_data, backend=default_backend())
else:
raise ValueError('无法解析公钥格式')
# 生成证书
now = get_utc_now()
cert = x509.CertificateBuilder().subject_name(
subject_name
).issuer_name(
ca_cert.subject
).public_key(
public_key
).serial_number(
x509.random_serial_number()
).not_valid_before(
now
).not_valid_after(
now + timedelta(days=Config.CERT_VALIDITY_DAYS)
).sign(ca_private_key, hashes.SHA256(), default_backend())
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
serial_number = str(cert.serial_number)
# 确保证书过期时间转换为naive datetimeMySQL DATETIME不支持时区
expire_time = cert.not_valid_after
if expire_time.tzinfo is not None:
# 如果证书中的时间带时区转换为UTC的naive datetime
expire_time = expire_time.astimezone(timezone.utc).replace(tzinfo=None)
return cert_pem, serial_number, expire_time
def sign_certificate(csr_pem, request_id):
"""使用CA私钥签署证书从CSR"""
csr = x509.load_pem_x509_csr(csr_pem.encode('utf-8'), default_backend())
ca_private_key = load_ca_private_key()
ca_cert = load_ca_certificate()
# 生成证书
now = get_utc_now()
cert = x509.CertificateBuilder().subject_name(
csr.subject
).issuer_name(
ca_cert.subject
).public_key(
csr.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
now
).not_valid_after(
now + timedelta(days=Config.CERT_VALIDITY_DAYS)
).sign(ca_private_key, hashes.SHA256(), default_backend())
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
serial_number = str(cert.serial_number)
# 确保证书过期时间转换为naive datetimeMySQL DATETIME不支持时区
expire_time = cert.not_valid_after
if expire_time.tzinfo is not None:
# 如果证书中的时间带时区转换为UTC的naive datetime
expire_time = expire_time.astimezone(timezone.utc).replace(tzinfo=None)
return cert_pem, serial_number, expire_time
def verify_certificate(cert_pem, check_crl=True):
"""验证证书是否由CA签发
Args:
cert_pem: 证书PEM格式字符串
check_crl: 是否检查证书吊销列表CRL默认为True
"""
try:
cert = x509.load_pem_x509_certificate(cert_pem.encode('utf-8'), default_backend())
ca_cert = load_ca_certificate()
# 验证证书是否过期
# 处理证书时间可能是naive或aware的情况
cert_expire_time = cert.not_valid_after
cert_start_time = cert.not_valid_before
# 统一转换为naive UTC datetime
if cert_expire_time.tzinfo is not None:
cert_expire_time = cert_expire_time.astimezone(timezone.utc).replace(tzinfo=None)
if cert_start_time.tzinfo is not None:
cert_start_time = cert_start_time.astimezone(timezone.utc).replace(tzinfo=None)
now = get_utc_now() # 返回naive UTC时间
# 检查证书是否已生效
if cert_start_time > now:
return False, f"证书尚未生效(生效时间: {cert_start_time.isoformat()}"
# 检查证书是否过期
if cert_expire_time < now:
return False, f"证书已过期(过期时间: {cert_expire_time.isoformat()}"
# 验证证书是否由CA签发简单验证实际应该验证签名
# 这里简化处理,检查发行者是否匹配
if cert.issuer != ca_cert.subject:
return False, "证书不是由本CA签发的"
# 检查证书是否在CRL证书吊销列表
if check_crl:
from models import Certificate, CRL, db
cert_serial_number = str(cert.serial_number)
# 通过序列号查找证书
certificate = Certificate.query.filter_by(
serial_number=cert_serial_number,
deleted_at=None
).first()
if certificate:
# 检查证书状态
if certificate.state == 2:
return False, "证书已被吊销"
# 检查是否在CRL中
crl_entry = CRL.query.filter_by(
certificate_id=certificate.id,
deleted_at=None
).first()
if crl_entry:
return False, "证书已被吊销在CRL中"
return True, "证书有效"
except Exception as e:
return False, f"证书验证失败: {str(e)}"
def parse_certificate(cert_pem):
"""解析证书信息"""
cert = x509.load_pem_x509_certificate(cert_pem.encode('utf-8'), default_backend())
# 处理证书时间统一转换为naive UTC datetime再格式化为ISO字符串
not_valid_before = cert.not_valid_before
not_valid_after = cert.not_valid_after
if not_valid_before.tzinfo is not None:
not_valid_before = not_valid_before.astimezone(timezone.utc).replace(tzinfo=None)
if not_valid_after.tzinfo is not None:
not_valid_after = not_valid_after.astimezone(timezone.utc).replace(tzinfo=None)
data = {
'serial_number': str(cert.serial_number),
'subject': {},
'issuer': {},
'not_valid_before': not_valid_before.isoformat(),
'not_valid_after': not_valid_after.isoformat(),
}
for attr in cert.subject:
if attr.oid == NameOID.COUNTRY_NAME:
data['subject']['country'] = attr.value
elif attr.oid == NameOID.STATE_OR_PROVINCE_NAME:
data['subject']['province'] = attr.value
elif attr.oid == NameOID.LOCALITY_NAME:
data['subject']['locality'] = attr.value
elif attr.oid == NameOID.ORGANIZATION_NAME:
data['subject']['organization'] = attr.value
elif attr.oid == NameOID.ORGANIZATIONAL_UNIT_NAME:
data['subject']['organization_unit_name'] = attr.value
elif attr.oid == NameOID.COMMON_NAME:
data['subject']['common_name'] = attr.value
elif attr.oid == NameOID.EMAIL_ADDRESS:
data['subject']['email_address'] = attr.value
for attr in cert.issuer:
if attr.oid == NameOID.COMMON_NAME:
data['issuer']['common_name'] = attr.value
elif attr.oid == NameOID.ORGANIZATION_NAME:
data['issuer']['organization'] = attr.value
return data

@ -0,0 +1,95 @@
import request from '@/utils/request'
export function getCertificates() {
return request({
url: '/certificate/list',
method: 'get'
})
}
export function getCertificateDetail(certId) {
return request({
url: `/certificate/detail/${certId}`,
method: 'get'
})
}
export function downloadCertificate(certId) {
return request({
url: `/certificate/download/${certId}`,
method: 'get',
responseType: 'blob'
})
}
export function revokeCertificate(certId) {
return request({
url: `/certificate/revoke/${certId}`,
method: 'post'
})
}
export function verifyCertificate(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/certificate/verify',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export function requestCertificate(data) {
return request({
url: '/certificate/request',
method: 'post',
data
})
}
export function uploadCSR(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/certificate/request/upload-csr',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export function submitKey(data) {
return request({
url: '/certificate/request/submit-key',
method: 'post',
data
})
}
export function generateKeyPair() {
return request({
url: '/certificate/generate-keypair',
method: 'post'
})
}
export function getRequestStatus() {
return request({
url: '/certificate/request/status',
method: 'get'
})
}

@ -0,0 +1,438 @@
from flask import Blueprint, request, jsonify, send_file
from models import Certificate, CARequest, CRL, db
from utils.auth_utils import verify_token
from utils.cert_utils import (
generate_key_pair, create_csr_from_data, parse_csr,
sign_certificate, verify_certificate, parse_certificate
)
from middleware.auth_middleware import login_required
from datetime import datetime, timezone
from cryptography.hazmat.primitives import serialization
import os
def get_beijing_now():
"""获取当前北京时间UTC+8用于数据库存储"""
from datetime import timezone, timedelta
beijing_tz = timezone(timedelta(hours=8))
utc_now = datetime.now(timezone.utc)
beijing_now = utc_now.astimezone(beijing_tz)
return beijing_now.replace(tzinfo=None)
cert_bp = Blueprint('certificate', __name__)
@cert_bp.route('/list', methods=['GET'])
@login_required
def get_certificates():
"""获取用户的所有证书"""
user_id = request.user_id
certificates = Certificate.query.filter_by(
user_id=user_id,
deleted_at=None
).order_by(Certificate.created_at.desc()).all()
cert_list = []
for cert in certificates:
cert_dict = cert.to_dict()
# 检查是否已过期(证书过期时间存储在数据库中,是北京时间)
# 如果证书在使用中但已过期,更新状态
if cert.expire_time and cert.expire_time < get_beijing_now() and cert.state == 1:
cert.state = 2 # 更新数据库状态为已过期
cert_dict['state'] = 2
cert_dict['state_text'] = '已过期'
db.session.commit()
cert_list.append(cert_dict)
return jsonify({
'code': 200,
'data': cert_list
}), 200
@cert_bp.route('/detail/<int:cert_id>', methods=['GET'])
@login_required
def get_certificate_detail(cert_id):
"""获取证书详细信息"""
user_id = request.user_id
cert = Certificate.query.filter_by(
id=cert_id,
user_id=user_id,
deleted_at=None
).first()
if not cert:
return jsonify({'code': 404, 'message': '证书不存在'}), 404
cert_dict = cert.to_dict()
# 解析证书内容
if cert.certificate_content:
cert_info = parse_certificate(cert.certificate_content)
cert_dict['certificate_info'] = cert_info
cert_dict['certificate_content'] = cert.certificate_content
return jsonify({
'code': 200,
'data': cert_dict
}), 200
@cert_bp.route('/download/<int:cert_id>', methods=['GET'])
@login_required
def download_certificate(cert_id):
"""下载证书"""
try:
user_id = request.user_id
cert = Certificate.query.filter_by(
id=cert_id,
user_id=user_id,
deleted_at=None
).first()
if not cert:
return jsonify({'code': 404, 'message': '证书不存在'}), 404
if not cert.certificate_content:
return jsonify({'code': 404, 'message': '证书内容为空'}), 404
# 使用StringIO直接在内存中创建文件不需要临时文件
from io import BytesIO
filename = f'certificate_{cert_id}.cer'
# 创建文件流
file_data = BytesIO()
file_data.write(cert.certificate_content.encode('utf-8'))
file_data.seek(0)
return send_file(
file_data,
mimetype='application/x-x509-ca-cert',
as_attachment=True,
download_name=filename
)
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'code': 500, 'message': f'下载失败: {str(e)}'}), 500
@cert_bp.route('/revoke/<int:cert_id>', methods=['POST'])
@login_required
def revoke_certificate(cert_id):
"""吊销证书"""
user_id = request.user_id
cert = Certificate.query.filter_by(
id=cert_id,
user_id=user_id,
deleted_at=None
).first()
if not cert:
return jsonify({'code': 404, 'message': '证书不存在'}), 404
if cert.state == 2:
return jsonify({'code': 400, 'message': '证书已被吊销'}), 400
# 更新证书状态
cert.state = 2
cert.updated_at = get_beijing_now()
# 添加到CRL
from datetime import timezone
crl = CRL(
certificate_id=cert_id,
input_time=int(datetime.now(timezone.utc).timestamp()) # CRL时间戳使用UTC
)
db.session.add(crl)
db.session.commit()
return jsonify({
'code': 200,
'message': '证书已吊销'
}), 200
@cert_bp.route('/verify', methods=['POST'])
@login_required
def verify_cert():
"""验证证书"""
if 'file' not in request.files:
return jsonify({'code': 400, 'message': '未上传文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'code': 400, 'message': '未选择文件'}), 400
# 支持多种证书文件格式
allowed_extensions = ['.cer', '.crt', '.pem', '.der']
if not any(file.filename.lower().endswith(ext) for ext in allowed_extensions):
return jsonify({'code': 400, 'message': '文件格式错误,请上传.cer、.crt、.pem或.der文件'}), 400
try:
file_content = file.read()
cert_pem = None
# 尝试判断文件格式PEM格式是文本DER格式是二进制
# PEM格式通常以"-----BEGIN"开头
from cryptography import x509
from cryptography.hazmat.backends import default_backend
# 首先尝试作为DER格式二进制处理
try:
cert = x509.load_der_x509_certificate(file_content, default_backend())
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
except Exception:
# 如果DER格式失败尝试作为PEM格式文本处理
try:
# 尝试UTF-8解码
cert_pem_text = file_content.decode('utf-8')
# 验证是否是PEM格式
if cert_pem_text.strip().startswith('-----BEGIN'):
cert_pem = cert_pem_text
else:
# 如果不是PEM格式尝试直接加载可能是其他编码
cert = x509.load_pem_x509_certificate(file_content, default_backend())
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
except UnicodeDecodeError:
# UTF-8解码失败说明是二进制格式但DER也失败了
# 尝试其他编码或直接作为PEM二进制处理
try:
cert = x509.load_pem_x509_certificate(file_content, default_backend())
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
except Exception as pem_error:
return jsonify({
'code': 400,
'message': f'无法解析证书文件。请确保文件是有效的PEM或DER格式证书。错误: {str(pem_error)}'
}), 400
except Exception as pem_error:
# PEM格式加载失败
return jsonify({
'code': 400,
'message': f'无法解析证书文件。请确保文件是有效的PEM或DER格式证书。错误: {str(pem_error)}'
}), 400
if not cert_pem:
return jsonify({'code': 400, 'message': '无法解析证书文件'}), 400
# 验证证书包括检查CRL
is_valid, message = verify_certificate(cert_pem, check_crl=True)
cert_info = None
if is_valid:
cert_info = parse_certificate(cert_pem)
return jsonify({
'code': 200,
'data': {
'is_valid': is_valid,
'message': message,
'certificate_info': cert_info
}
}), 200
except Exception as e:
return jsonify({'code': 500, 'message': f'验证失败: {str(e)}'}), 500
@cert_bp.route('/request', methods=['POST'])
@login_required
def request_certificate():
"""申请证书(第一步:提交基本信息)"""
user_id = request.user_id
data = request.get_json()
# 验证必填字段
if not data.get('common_name'):
return jsonify({'code': 400, 'message': '域名为必填项'}), 400
# 检查是否有待审核的请求
existing_request = CARequest.query.filter_by(
user_id=user_id,
state=1,
deleted_at=None
).first()
if existing_request:
return jsonify({'code': 400, 'message': '您有正在审核中的申请,请等待审核完成'}), 400
# 创建证书请求
cert_request = CARequest(
user_id=user_id,
state=1, # 待审核
country=data.get('country'),
province=data.get('province'),
locality=data.get('locality'),
organization=data.get('organization'),
organization_unit_name=data.get('organization_unit_name'),
common_name=data.get('common_name'),
email_address=data.get('email_address'),
public_key='', # 第二步提交
csr_content=data.get('csr_content') # 如果上传了CSR
)
db.session.add(cert_request)
db.session.commit()
return jsonify({
'code': 200,
'message': '基本信息提交成功,请继续提交公钥',
'data': {
'request_id': cert_request.id
}
}), 200
@cert_bp.route('/request/upload-csr', methods=['POST'])
@login_required
def upload_csr():
"""上传CSR文件"""
user_id = request.user_id
if 'file' not in request.files:
return jsonify({'code': 400, 'message': '未上传文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'code': 400, 'message': '未选择文件'}), 400
if not file.filename.endswith('.csr'):
return jsonify({'code': 400, 'message': '文件格式错误,请上传.csr文件'}), 400
try:
csr_pem = file.read().decode('utf-8')
data, public_key = parse_csr(csr_pem)
# 检查是否有待审核的请求
existing_request = CARequest.query.filter_by(
user_id=user_id,
state=1,
deleted_at=None
).order_by(CARequest.created_at.desc()).first()
if existing_request:
# 更新现有请求
existing_request.csr_content = csr_pem
existing_request.public_key = public_key
existing_request.country = data.get('country')
existing_request.province = data.get('province')
existing_request.locality = data.get('locality')
existing_request.organization = data.get('organization')
existing_request.organization_unit_name = data.get('organization_unit_name')
existing_request.common_name = data.get('common_name')
existing_request.email_address = data.get('email_address')
existing_request.state = 1 # 待审核
else:
# 创建新请求
existing_request = CARequest(
user_id=user_id,
state=1,
csr_content=csr_pem,
public_key=public_key,
country=data.get('country'),
province=data.get('province'),
locality=data.get('locality'),
organization=data.get('organization'),
organization_unit_name=data.get('organization_unit_name'),
common_name=data.get('common_name'),
email_address=data.get('email_address')
)
db.session.add(existing_request)
db.session.commit()
return jsonify({
'code': 200,
'message': 'CSR文件上传成功等待审核',
'data': {
'request_id': existing_request.id,
'parsed_data': data
}
}), 200
except Exception as e:
return jsonify({'code': 500, 'message': f'解析CSR失败: {str(e)}'}), 500
@cert_bp.route('/request/submit-key', methods=['POST'])
@login_required
def submit_key():
"""提交公钥(第二步)"""
user_id = request.user_id
data = request.get_json()
request_id = data.get('request_id')
public_key = data.get('public_key')
private_key = data.get('private_key') # 可选,如果自动生成
if not request_id:
return jsonify({'code': 400, 'message': '请求ID不能为空'}), 400
cert_request = CARequest.query.filter_by(
id=request_id,
user_id=user_id,
deleted_at=None
).first()
if not cert_request:
return jsonify({'code': 404, 'message': '证书请求不存在'}), 404
if cert_request.state != 1:
return jsonify({'code': 400, 'message': '该请求已处理,无法修改'}), 400
# 如果提供了私钥创建CSR
if private_key and not cert_request.csr_content:
try:
csr, _ = create_csr_from_data({
'private_key': private_key,
'country': cert_request.country,
'province': cert_request.province,
'locality': cert_request.locality,
'organization': cert_request.organization,
'organization_unit_name': cert_request.organization_unit_name,
'common_name': cert_request.common_name,
'email_address': cert_request.email_address
})
cert_request.csr_content = csr.public_bytes(serialization.Encoding.PEM).decode('utf-8')
except Exception as e:
return jsonify({'code': 500, 'message': f'创建CSR失败: {str(e)}'}), 500
# 更新公钥
if public_key:
cert_request.public_key = public_key
cert_request.state = 1 # 保持待审核状态
db.session.commit()
return jsonify({
'code': 200,
'message': '公钥提交成功,等待审核'
}), 200
@cert_bp.route('/generate-keypair', methods=['POST'])
@login_required
def generate_keypair():
"""自动生成密钥对"""
try:
private_key, public_key = generate_key_pair()
return jsonify({
'code': 200,
'data': {
'private_key': private_key,
'public_key': public_key
}
}), 200
except Exception as e:
return jsonify({'code': 500, 'message': f'生成密钥对失败: {str(e)}'}), 500
@cert_bp.route('/request/status', methods=['GET'])
@login_required
def get_request_status():
"""获取证书申请状态"""
user_id = request.user_id
requests = CARequest.query.filter_by(
user_id=user_id,
deleted_at=None
).order_by(CARequest.created_at.desc()).all()
request_list = [req.to_dict() for req in requests]
return jsonify({
'code': 200,
'data': request_list
}), 200

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDMjCCAhqgAwIBAgIUHcRmJvqR4rcL5twSbYBXpCjQorgwDQYJKoZIhvcNAQEL
BQAwZTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl
aWppbmcxGTAXBgNVBAoMEFNpbXBsZSBDQSBTeXN0ZW0xFzAVBgNVBAMMDlNpbXBs
ZSBDQSBSb290MB4XDTI1MTIyOTAzMDUxMVoXDTI2MTIyOTAzMDUxMVowQTELMAkG
A1UEBhMCQ04xCzAJBgNVBAgMAmd4MQ8wDQYDVQQKDAbmoYLnlLUxFDASBgNVBAMM
C2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv6jI
oBvsgdi+TYktAFj9l3Ad+z0+sAYpyOv6MNDcgHX32eH+/Qm+6dhdp6LQgQ1hWfTW
GE5eX4N/0R09zELDqaNUWXalZuk5k7K/Kiqsrt7CknDzE5FHdZBVImUdjBc7cqLr
bfhDiIfeLWKUD5sMBPcON/dGwzvA6GsY74Zljx6NBBIpC/BnN+/DFLBUr0R4bi5f
ViOCVCLJn2cznm/31sg+izhh53ulLfIb3NOiPkQj6JZkYT/hrqqS6nmA+o8FQgeF
HNDqxdXBMKOx3DHDHZl50aER5U0DB6cOA6Mc7kJoNKw0yxPaGueW9gXV9CouodSk
qD+YYtvOo1+hr9/DSQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCOR8Uk7gNEo+u9
afiOEUxwRfO6iN5+qRuOPCgPzrsYyLDa3jeWD1C4x8GdFQxMp/Jiivj760oYMl3Q
tYzhBpwgRKMuw9glHV1SX8HeaU8wgBGx/gxSU1zcf/XEDkCdiubcKJPmzqBr3+TM
eighF7YgiCBuet19Nk2wLYF+0DdfmZ6ntEMv2ZYcScqYBudz1+CfTZ3h7p8djL8R
z4a3bSMc9PuA94QeLkHqYYM6HA0IsCXwCLDgCfzCwHDkXSlECzauVr6DsQyQOX9A
QD/pki3gbFKZ1NHu75T4k99u2TglS8wiE+fgmJHNLfPVuqXa1wv605qhJ+lHKiPW
MpFulZZC
-----END CERTIFICATE-----

@ -0,0 +1,57 @@
-- 数据库补充SQL如果表已存在使用此脚本添加缺失的字段
-- 注意:如果表不存在,请先执行 init_database.sql
USE Simple_CA;
-- 为certificates表添加证书内容字段和序列号字段如果不存在
SET @dbname = DATABASE();
SET @tablename = "certificates";
SET @columnname = "certificate_content";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
"SELECT 1",
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN `certificate_content` TEXT NULL COMMENT '证书内容(PEM格式)' AFTER `expire_time`")
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
SET @columnname = "serial_number";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
"SELECT 1",
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN `serial_number` VARCHAR(64) NULL COMMENT '证书序列号' AFTER `certificate_content`")
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- 为ca_requests表添加CSR文件内容字段如果不存在
SET @tablename = "ca_requests";
SET @columnname = "csr_content";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
"SELECT 1",
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN `csr_content` TEXT NULL COMMENT 'CSR文件内容' AFTER `public_key`")
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

@ -0,0 +1,20 @@
-- 修复user_tokens表token字段长度的SQL脚本
-- 执行方式: mysql -u root -p Simple_CA < fix_token_length.sql
USE Simple_CA;
-- 修改token字段长度从64增加到512
ALTER TABLE `user_tokens`
MODIFY COLUMN `token` VARCHAR(512) NOT NULL COMMENT 'JWT Token';
-- 验证修改
DESCRIBE `user_tokens`;

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小型CA系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,80 @@
import { createRouter, createWebHistory } from 'vue-router'
import { getToken, getUserInfo } from '@/utils/auth'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
redirect: '/certificates',
meta: { requiresAuth: true },
children: [
{
path: 'certificates',
name: 'Certificates',
component: () => import('@/views/Certificates.vue')
},
{
path: 'certificate/register',
name: 'RegisterCertificate',
component: () => import('@/views/RegisterCertificate.vue')
},
{
path: 'certificate/verify',
name: 'VerifyCertificate',
component: () => import('@/views/VerifyCertificate.vue')
},
{
path: 'admin/requests',
name: 'AdminRequests',
component: () => import('@/views/admin/Requests.vue'),
meta: { requiresAdmin: true }
},
{
path: 'admin/certificates',
name: 'AdminCertificates',
component: () => import('@/views/admin/Certificates.vue'),
meta: { requiresAdmin: true }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const token = getToken()
const userInfo = getUserInfo()
if (to.meta.requiresAuth && !token) {
next('/login')
} else if (!to.meta.requiresAuth && token && (to.path === '/login' || to.path === '/register')) {
next('/')
} else if (to.meta.requiresAdmin) {
// 检查管理员权限
if (!userInfo || userInfo.authority !== 1) {
next('/certificates') // 非管理员跳转到证书列表
} else {
next()
}
} else {
next()
}
})
export default router

@ -0,0 +1,89 @@
-- 完整的数据库初始化脚本
-- 使用前请先创建数据库: CREATE DATABASE Simple_CA DEFAULT CHARACTER SET = 'utf8mb4';
USE Simple_CA;
-- 如果表已存在则删除(谨慎使用)
-- DROP TABLE IF EXISTS `crls`;
-- DROP TABLE IF EXISTS `certificates`;
-- DROP TABLE IF EXISTS `ca_requests`;
-- DROP TABLE IF EXISTS `user_tokens`;
-- DROP TABLE IF EXISTS `users`;
-- 创建用户表
CREATE TABLE IF NOT EXISTS `users` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`username` varchar(16) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`authority` int DEFAULT NULL COMMENT '权限1表示系统管理员',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
KEY `idx_users_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
-- 创建用户Token表
CREATE TABLE IF NOT EXISTS `user_tokens` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` datetime DEFAULT NULL,
`user_id` int NOT NULL,
`token` varchar(512) NOT NULL COMMENT 'JWT Token',
`expire_time` bigint NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_tokens_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户token表';
-- 创建CA请求表
CREATE TABLE IF NOT EXISTS `ca_requests` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime DEFAULT NULL COMMENT '字段创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
`user_id` int NOT NULL COMMENT '申请证书的用户ID',
`state` int unsigned NOT NULL COMMENT '证书状态1待审核 2 审核通过, 3审核未通过',
`public_key` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '公钥',
`csr_content` text CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'CSR文件内容',
`country` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '国家',
`province` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '州市',
`locality` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '地区',
`organization` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '组织',
`organization_unit_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '部门',
`common_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '姓名',
`email_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`),
KEY `idx_ca_requests_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='请求列表';
-- 创建证书表
CREATE TABLE IF NOT EXISTS `certificates` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`user_id` int NOT NULL COMMENT '证书拥有者ID',
`state` int unsigned NOT NULL COMMENT '状态1 代表在使用中2代表已撤销或过期',
`request_id` int NOT NULL COMMENT '证书请求ID',
`expire_time` datetime NOT NULL COMMENT '过期时间戳',
`certificate_content` text CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '证书内容(PEM格式)',
`serial_number` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '证书序列号',
PRIMARY KEY (`id`),
KEY `idx_certificates_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='证书列表';
-- 创建证书吊销列表表
CREATE TABLE IF NOT EXISTS `crls` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`certificate_id` int NOT NULL COMMENT '证书ID',
`input_time` bigint NOT NULL COMMENT '加入时间戳',
PRIMARY KEY (`id`),
KEY `idx_crls_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='证书吊销列表';

@ -0,0 +1,26 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus)
app.mount('#app')

1701
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,31 @@
{
"name": "ca-system-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev": "vite",
"build:vite": "vite build"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.5",
"axios": "^1.6.0",
"element-plus": "^2.4.4",
"@element-plus/icons-vue": "^2.3.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"vite": "^5.0.5"
}
}

@ -0,0 +1,38 @@
/**
* 性能优化工具函数
* 防抖函数
*/
export function debounce(func, wait = 300) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
/**
* 节流函数
*/
export function throttle(func, limit = 300) {
let inThrottle
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}

@ -0,0 +1,84 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getToken, removeToken } from './auth'
import router from '@/router'
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(
config => {
const token = getToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
// 如果是文件下载blob类型直接返回response
if (response.config.responseType === 'blob' || response.data instanceof Blob) {
return response.data
}
const res = response.data
if (res.code === 401) {
removeToken()
router.push('/login')
ElMessage.error(res.message || '登录已过期,请重新登录')
return Promise.reject(new Error(res.message || '登录已过期'))
}
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
error => {
console.error('请求错误:', error)
// 处理blob类型的错误响应可能是文件下载失败
if (error.response && error.response.data instanceof Blob) {
// 异步解析blob错误信息
error.response.data.text().then(text => {
try {
const errorData = JSON.parse(text)
ElMessage.error(errorData.message || '请求失败')
} catch (e) {
ElMessage.error('请求失败')
}
}).catch(() => {
ElMessage.error('请求失败')
})
return Promise.reject(error)
}
if (error.code === 'ECONNREFUSED' || error.message.includes('ECONNREFUSED')) {
ElMessage.error('无法连接到服务器,请确保后端服务正在运行')
} else if (error.response) {
// 服务器返回了错误响应
const message = error.response.data?.message || error.message || '请求失败'
ElMessage.error(message)
} else if (error.request) {
// 请求已发送但没有收到响应
ElMessage.error('服务器无响应,请检查后端服务是否正常运行')
} else {
ElMessage.error(error.message || '请求失败')
}
return Promise.reject(error)
}
)
export default service

@ -0,0 +1,33 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 8080,
host: '127.0.0.1', // 强制使用IPv4
proxy: {
'/api': {
target: 'http://127.0.0.1:5000', // 使用IPv4地址而不是localhost
changeOrigin: true,
secure: false,
ws: true, // 支持websocket
configure: (proxy, _options) => {
proxy.on('error', (err, _req, _res) => {
console.log('代理错误:', err);
});
proxy.on('proxyReq', (proxyReq, req, _res) => {
console.log('发送请求到后端:', req.method, req.url);
});
},
}
}
}
})
Loading…
Cancel
Save