Procházet zdrojové kódy

支持证书配置

liuq před 3 měsíci
rodič
revize
53348dee81

+ 2 - 1
backend/app/api/v1/api.py

@@ -1,6 +1,6 @@
 from fastapi import APIRouter
 
-from app.api.v1.endpoints import auth, users, apps, utils, simple_auth, oidc, open_api, logs, system_logs, backup, login_logs, user_import
+from app.api.v1.endpoints import auth, users, apps, utils, simple_auth, oidc, open_api, logs, system_logs, backup, login_logs, user_import, system
 
 api_router = APIRouter()
 api_router.include_router(auth.router, prefix="/auth", tags=["认证 (Auth)"])
@@ -11,6 +11,7 @@ api_router.include_router(logs.router, prefix="/logs", tags=["操作日志 (Logs
 api_router.include_router(login_logs.router, prefix="/login-logs", tags=["登录日志 (Login Logs)"])
 api_router.include_router(system_logs.router, prefix="/system-logs", tags=["后台日志 (System Logs)"])
 api_router.include_router(backup.router, prefix="/backups", tags=["数据备份 (Backup)"])
+api_router.include_router(system.router, prefix="/system", tags=["系统配置 (System)"])
 api_router.include_router(utils.router, prefix="/utils", tags=["工具 (Utils)"])
 api_router.include_router(simple_auth.router, prefix="/simple", tags=["简易认证 (SimpleAuth)"])
 api_router.include_router(oidc.router, prefix="/oidc", tags=["OIDC (OpenID Connect)"])

+ 55 - 0
backend/app/api/v1/endpoints/system.py

@@ -0,0 +1,55 @@
+import os
+import shutil
+from typing import Annotated
+
+from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
+from app.api.v1 import deps
+from app.models.user import User, UserRole
+
+router = APIRouter()
+
+CERTS_DIR = "/app/certs"
+CRT_FILENAME = "server.crt"
+KEY_FILENAME = "server.key"
+
+@router.post("/ssl/config", summary="更新SSL证书")
+async def update_ssl_config(
+    crt_file: Annotated[UploadFile, File(description="证书文件 (.crt/.pem)")],
+    key_file: Annotated[UploadFile, File(description="私钥文件 (.key)")],
+    current_user: User = Depends(deps.get_current_active_user),
+):
+    """
+    上传并更新SSL证书。
+    证书文件将保存到共享卷中,Nginx会自动检测变化并重载。
+    需要超级管理员权限。
+    """
+    if current_user.role != UserRole.SUPER_ADMIN:
+        raise HTTPException(status_code=403, detail="权限不足")
+
+    # 确保目录存在
+    os.makedirs(CERTS_DIR, exist_ok=True)
+    
+    crt_path = os.path.join(CERTS_DIR, CRT_FILENAME)
+    key_path = os.path.join(CERTS_DIR, KEY_FILENAME)
+    
+    # 简单的文件扩展名检查 (可以根据需要增强)
+    if not crt_file.filename.endswith(('.crt', '.pem', '.cer')):
+        raise HTTPException(status_code=400, detail="证书文件格式不正确,请上传 .crt, .pem 或 .cer 文件")
+    
+    if not key_file.filename.endswith(('.key', '.pem')):
+         raise HTTPException(status_code=400, detail="私钥文件格式不正确,请上传 .key 或 .pem 文件")
+
+    try:
+        # 保存证书文件
+        with open(crt_path, "wb") as buffer:
+            shutil.copyfileobj(crt_file.file, buffer)
+            
+        # 保存私钥文件
+        with open(key_path, "wb") as buffer:
+            shutil.copyfileobj(key_file.file, buffer)
+            
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"保存证书文件失败: {str(e)}")
+        
+    return {"message": "SSL证书已更新,Nginx将自动重新加载配置。"}
+

+ 5 - 0
docker-compose.wsl.yml

@@ -29,8 +29,11 @@ services:
       target: production-stage
     ports:
       - "80:80"
+      - "443:443"
     depends_on:
       - backend
+    volumes:
+      - certs_data:/etc/nginx/certs
     restart: always
 
   # ==========================================
@@ -65,6 +68,7 @@ services:
         condition: service_completed_successfully
     volumes:
       - ./backend:/app # Hot Reload for Backend too
+      - certs_data:/app/certs
     restart: always
 
   # ==========================================
@@ -205,3 +209,4 @@ services:
 volumes:
   db_data:
   postgres_data:
+  certs_data:

+ 5 - 0
docker-compose.yml

@@ -29,8 +29,11 @@ services:
       target: production-stage
     ports:
       - "80:80"
+      - "443:443"
     depends_on:
       - backend
+    volumes:
+      - certs_data:/etc/nginx/certs
     restart: always
 
   # ==========================================
@@ -65,6 +68,7 @@ services:
         condition: service_completed_successfully
     volumes:
       - ./backend:/app # Hot Reload for Backend too
+      - certs_data:/app/certs
     restart: always
 
   # ==========================================
@@ -205,3 +209,4 @@ services:
 volumes:
   db_data:
   postgres_data:
+  certs_data:

+ 9 - 2
frontend/Dockerfile

@@ -18,10 +18,17 @@ RUN npm run build
 
 # Production Stage with Nginx
 FROM nginx:stable-alpine as production-stage
+
+# Install openssl and inotify-tools for dynamic SSL reloading
+RUN apk add --no-cache openssl inotify-tools
+
 COPY --from=build-stage /app/dist /usr/share/nginx/html
 COPY nginx.conf /etc/nginx/conf.d/default.conf
-EXPOSE 80
-CMD ["nginx", "-g", "daemon off;"]
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+EXPOSE 80 443
+ENTRYPOINT ["/entrypoint.sh"]
 
 # Development Stage
 FROM node:18-alpine as dev-stage

+ 40 - 0
frontend/entrypoint.sh

@@ -0,0 +1,40 @@
+#!/bin/sh
+
+# 定义证书路径
+CERT_DIR="/etc/nginx/certs"
+CRT_FILE="$CERT_DIR/server.crt"
+KEY_FILE="$CERT_DIR/server.key"
+
+# 确保目录存在
+mkdir -p "$CERT_DIR"
+
+# 如果证书不存在,生成自签名证书以防止Nginx启动失败
+if [ ! -f "$CRT_FILE" ] || [ ! -f "$KEY_FILE" ]; then
+    echo "SSL certificates not found. Generating self-signed certificates..."
+    openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+        -keyout "$KEY_FILE" \
+        -out "$CRT_FILE" \
+        -subj "/C=CN/ST=State/L=City/O=Organization/CN=localhost"
+    echo "Self-signed certificates generated."
+fi
+
+# 启动 Nginx (后台运行)
+echo "Starting Nginx..."
+nginx -g "daemon on;"
+
+# 启动文件监控循环
+echo "Starting SSL certificate monitor..."
+while true; do
+    # 监控 /etc/nginx/certs 目录下的 modify, move, create, delete 事件
+    inotifywait -e modify,move,create,delete -r "$CERT_DIR"
+    
+    echo "Certificate change detected. Testing configuration..."
+    nginx -t
+    if [ $? -eq 0 ]; then
+        echo "Configuration valid. Reloading Nginx..."
+        nginx -s reload
+    else
+        echo "Configuration invalid. Skipping reload."
+    fi
+done
+

+ 35 - 0
frontend/nginx.conf

@@ -23,3 +23,38 @@ server {
     }
 }
 
+server {
+    listen 443 ssl;
+    server_name localhost;
+
+    ssl_certificate /etc/nginx/certs/server.crt;
+    ssl_certificate_key /etc/nginx/certs/server.key;
+
+    # SSL Settings
+    ssl_session_timeout 1d;
+    ssl_session_cache shared:SSL:50m;
+    ssl_session_tickets off;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+    ssl_prefer_server_ciphers off;
+
+    # Serve Static Files
+    location / {
+        root /usr/share/nginx/html;
+        index index.html index.htm;
+        try_files $uri $uri/ /index.html;
+    }
+
+    # Proxy API requests to Backend
+    location /api/ {
+        proxy_pass http://backend:8000/api/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        
+        # WebSocket Support
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+}

+ 6 - 0
frontend/src/router/index.ts

@@ -101,6 +101,12 @@ const routes: Array<RouteRecordRaw> = [
         component: () => import('../views/admin/maintenance/DataRestore.vue'),
         meta: { requiresAdmin: true }
       },
+      {
+        path: 'ssl-config',
+        name: 'SSLConfig',
+        component: () => import('../views/admin/maintenance/SSLConfig.vue'),
+        meta: { requiresAdmin: true }
+      },
       {
         path: 'changelog',
         name: 'Changelog',

+ 5 - 1
frontend/src/views/Dashboard.vue

@@ -55,6 +55,10 @@
               <el-icon><RefreshRight /></el-icon>
               <span>数据还原</span>
             </el-menu-item>
+            <el-menu-item index="/dashboard/ssl-config">
+              <el-icon><Lock /></el-icon>
+              <span>证书配置</span>
+            </el-menu-item>
           </el-sub-menu>
 
           <el-menu-item 
@@ -121,7 +125,7 @@
 import { computed, onMounted, ref, reactive } from 'vue'
 import { useRouter } from 'vue-router'
 import { useAuthStore } from '../store/auth'
-import { Grid, List, QuestionFilled, User, ArrowDown, Connection, Monitor, Document, Download, RefreshRight } from '@element-plus/icons-vue'
+import { Grid, List, QuestionFilled, User, ArrowDown, Connection, Monitor, Document, Download, RefreshRight, Lock } from '@element-plus/icons-vue'
 import { ElMessage, FormInstance, FormRules } from 'element-plus'
 import api from '../utils/request'
 

+ 123 - 0
frontend/src/views/admin/maintenance/SSLConfig.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="app-container">
+    <el-card class="box-card">
+      <template #header>
+        <div class="card-header">
+          <span>HTTPS 证书配置</span>
+        </div>
+      </template>
+      
+      <div class="ssl-config-container">
+        <el-alert
+          title="注意:更新证书后 Nginx 服务会自动重载,可能会导致短暂的连接中断。"
+          type="warning"
+          show-icon
+          :closable="false"
+          style="margin-bottom: 20px"
+        />
+
+        <el-form label-width="120px" style="max-width: 600px">
+          <el-form-item label="证书文件">
+            <input 
+              type="file" 
+              class="file-input"
+              @change="handleCertFile" 
+              accept=".crt,.pem,.cer" 
+            />
+            <div class="tip">请上传 .crt 或 .pem 格式的证书文件</div>
+          </el-form-item>
+          
+          <el-form-item label="私钥文件">
+            <input 
+              type="file" 
+              class="file-input"
+              @change="handleKeyFile" 
+              accept=".key,.pem" 
+            />
+            <div class="tip">请上传 .key 或 .pem 格式的私钥文件</div>
+          </el-form-item>
+          
+          <el-form-item>
+            <el-button type="primary" @click="submitSSL" :loading="loading">
+              更新证书并重载 Nginx
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import request from '../../../utils/request'
+
+const certFile = ref<File | null>(null)
+const keyFile = ref<File | null>(null)
+const loading = ref(false)
+
+const handleCertFile = (event: Event) => {
+  const target = event.target as HTMLInputElement
+  if (target.files && target.files.length > 0) {
+    certFile.value = target.files[0]
+  }
+}
+
+const handleKeyFile = (event: Event) => {
+  const target = event.target as HTMLInputElement
+  if (target.files && target.files.length > 0) {
+    keyFile.value = target.files[0]
+  }
+}
+
+const submitSSL = async () => {
+  if (!certFile.value || !keyFile.value) {
+    ElMessage.warning('请同时选择证书文件和私钥文件')
+    return
+  }
+  
+  const formData = new FormData()
+  formData.append('crt_file', certFile.value)
+  formData.append('key_file', keyFile.value)
+  
+  loading.value = true
+  try {
+    await request.post('/system/ssl/config', formData, {
+      headers: { 'Content-Type': 'multipart/form-data' }
+    })
+    ElMessage.success('证书更新成功,Nginx 正在后台重载...')
+    // Reset inputs if needed, or leave them
+  } catch (error) {
+    console.error(error)
+    // Error is usually handled by request interceptor, but we can double check
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+.box-card {
+  width: 100%;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+  margin-top: 5px;
+}
+.file-input {
+  display: block;
+  padding: 5px 0;
+}
+</style>
+