feat: 审批流程模块 (T审批A审批)
- 新增 backend/app/api/approval.py 审批API - 前端H5支持发起审批、审批操作 - 添加审批卡片弹窗组件 - 路由注册审批模块
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
# CORS / CSP / 安全 Header 全套审计与改进
|
||||
|
||||
**审计日期**: 2026-06-15
|
||||
**审计人**: Claude(满载跑批)
|
||||
**关联**: [[风险跟踪表]] / [[后端架构]] / [[外部系统集成]]
|
||||
|
||||
---
|
||||
|
||||
## 📌 1. 现状盘点
|
||||
|
||||
### 1.1 后端 CORS 配置(`backend/app/main.py:363`)
|
||||
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins_list, # 逗号分隔的列表
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Authorization", "Content-Type", "X-Employee-Id"],
|
||||
)
|
||||
```
|
||||
|
||||
**当前 `cors_origins`**:
|
||||
- 默认: `localhost:5173,5174,5175`(开发)
|
||||
- 生产: `itsupport.servyou.com.cn`(.env.production)
|
||||
|
||||
### 1.2 Nginx 安全头(`nginx.conf` + `nginx-nas.conf`)
|
||||
|
||||
**已有**:
|
||||
```nginx
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
```
|
||||
|
||||
**缺失**:
|
||||
- `Strict-Transport-Security` (HSTS)
|
||||
- `Content-Security-Policy` (CSP)
|
||||
- `Referrer-Policy`
|
||||
- `Permissions-Policy`
|
||||
- `Cross-Origin-*` 系列
|
||||
|
||||
### 1.3 问题清单
|
||||
|
||||
| # | 问题 | 严重度 | 风险 |
|
||||
|---|---|---|---|
|
||||
| C-1 | CORS `allow_origins` 默认含 `*`(环境切换不当会泄露) | 🟠 | 跨域未授权 |
|
||||
| C-2 | CORS 没限制 `expose_headers`(前端拿不到 trace_id) | 🟡 | 排障不便 |
|
||||
| C-3 | CORS `max_age` 未设(每次预检) | 🟢 | 性能 |
|
||||
| C-4 | nginx 缺 HSTS | 🟠 | 中间人降级 |
|
||||
| C-5 | nginx 缺 CSP | 🟠 | XSS |
|
||||
| C-6 | nginx 缺 Referrer-Policy | 🟡 | 信息泄露 |
|
||||
| C-7 | nginx 缺 Permissions-Policy | 🟡 | 设备 API 滥用 |
|
||||
| C-8 | nginx 缺 COOP/COEP | 🟡 | 跨源攻击 |
|
||||
| C-9 | `/api/wecom/callback` 没限 IP | 🟡 | 恶意回调 |
|
||||
| C-10 | 4 前端没 CSP meta(防 XSS) | 🟠 | XSS |
|
||||
|
||||
---
|
||||
|
||||
## 📌 2. CORS 改进
|
||||
|
||||
### 2.1 后端 - 精细化 CORS
|
||||
|
||||
**新建 `backend/app/utils/cors_config.py`**:
|
||||
```python
|
||||
from typing import List
|
||||
from app.config import settings
|
||||
|
||||
|
||||
def build_cors_config() -> dict:
|
||||
"""根据环境构建 CORS 配置"""
|
||||
is_prod = settings.backend_env == "production" # 需新增环境变量
|
||||
|
||||
if is_prod:
|
||||
# 生产:严格白名单
|
||||
origins = [
|
||||
o.strip() for o in settings.cors_origins.split(",")
|
||||
if o.strip() and not o.startswith("*")
|
||||
]
|
||||
return {
|
||||
"allow_origins": origins,
|
||||
"allow_credentials": True,
|
||||
"allow_methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
"allow_headers": [
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Employee-Id",
|
||||
"X-Request-ID", # trace_id
|
||||
"X-CSRF-Token", # CSRF 防护
|
||||
"X-Agent-Id", # 坐席 ID
|
||||
],
|
||||
"expose_headers": [
|
||||
"X-Request-ID", # 暴露 trace_id
|
||||
"X-RateLimit-Remaining", # 限流剩余
|
||||
"X-RateLimit-Reset", # 限流重置
|
||||
],
|
||||
"max_age": 600, # 10 分钟预检缓存
|
||||
}
|
||||
|
||||
# 开发:宽松
|
||||
return {
|
||||
"allow_origins": settings.cors_origins_list,
|
||||
"allow_credentials": True,
|
||||
"allow_methods": ["*"],
|
||||
"allow_headers": ["*"],
|
||||
"expose_headers": ["*"],
|
||||
"max_age": 3600,
|
||||
}
|
||||
```
|
||||
|
||||
**更新 `main.py`**:
|
||||
```python
|
||||
from app.utils.cors_config import build_cors_config
|
||||
|
||||
cors_config = build_cors_config()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
**cors_config,
|
||||
)
|
||||
```
|
||||
|
||||
### 2.2 新增环境变量
|
||||
|
||||
**`backend/app/config.py`**:
|
||||
```python
|
||||
# 新增
|
||||
backend_env: str = "development" # development / production
|
||||
```
|
||||
|
||||
**`.env.production`**:
|
||||
```bash
|
||||
BACKEND_ENV=production
|
||||
CORS_ORIGINS=https://itsupport.servyou.com.cn
|
||||
```
|
||||
|
||||
### 2.3 CORS 验证脚本
|
||||
|
||||
```bash
|
||||
# 验证 CORS 头
|
||||
curl -I -X OPTIONS \
|
||||
-H "Origin: https://itsupport.servyou.com.cn" \
|
||||
-H "Access-Control-Request-Method: POST" \
|
||||
-H "Access-Control-Request-Headers: Authorization" \
|
||||
http://localhost:8000/api/v1/auth/login
|
||||
|
||||
# 期望响应:
|
||||
# Access-Control-Allow-Origin: https://itsupport.servyou.com.cn
|
||||
# Access-Control-Allow-Credentials: true
|
||||
# Access-Control-Max-Age: 600
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 3. Nginx 安全 Header 完整套
|
||||
|
||||
### 3.1 完整版 nginx.conf(替换安全头部分)
|
||||
|
||||
```nginx
|
||||
# =================================================================
|
||||
# 安全响应头配置(全部)
|
||||
# =================================================================
|
||||
|
||||
# 1. HSTS - 强制 HTTPS(2 年,包含子域名)
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
|
||||
# 2. CSP - 内容安全策略(严格版,API 网关除外)
|
||||
# 注意:API 路径不要 CSP(纯 JSON),只 HTML 路径需要
|
||||
location /itdesk/ {
|
||||
# 基础 CSP
|
||||
add_header Content-Security-Policy "
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://res.wx.qq.com;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob: https: http:;
|
||||
font-src 'self' data:;
|
||||
connect-src 'self' https://qyapi.weixin.qq.com wss://* https://*.servyou-it.com;
|
||||
media-src 'self' blob:;
|
||||
object-src 'none';
|
||||
frame-ancestors 'none';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
upgrade-insecure-requests;
|
||||
" always;
|
||||
|
||||
alias /usr/share/nginx/html/itdesk/;
|
||||
# ...
|
||||
}
|
||||
|
||||
# 3. 防 MIME 嗅探
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
# 4. 防点击劫持(更严:拒绝所有 frame 嵌入)
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
|
||||
# 5. XSS 过滤器(现代浏览器已废弃,保留向后兼容)
|
||||
add_header X-XSS-Protection "0" always; # 0 = 关闭(CSP 已接管)
|
||||
|
||||
# 6. Referrer 策略(API 不发送 referrer,HTML 限制来源)
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# 7. Permissions Policy(禁用不用的设备 API)
|
||||
add_header Permissions-Policy "
|
||||
camera=(),
|
||||
microphone=(),
|
||||
geolocation=(),
|
||||
payment=(),
|
||||
usb=(),
|
||||
magnetometer=(),
|
||||
gyroscope=(),
|
||||
accelerometer=()
|
||||
" always;
|
||||
|
||||
# 8. 跨源隔离
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
|
||||
# 9. 服务器信息隐藏
|
||||
server_tokens off; # 隐藏 nginx 版本
|
||||
|
||||
# 10. API 路径特殊头(API 不需要 CSP,但要 CORS 友好)
|
||||
location /api/ {
|
||||
# 移除 CSP(API 返回 JSON,不要 CSP)
|
||||
more_clear_headers "Content-Security-Policy";
|
||||
|
||||
# API 也加 HSTS
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Cache-Control "no-store" always; # API 禁止缓存
|
||||
|
||||
proxy_pass http://backend_api/;
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 完整版 nginx-nas.conf(同上,Cloudflare 适配)
|
||||
|
||||
```nginx
|
||||
# Cloudflare Tunnel 已经在外层 HTTPS,这里加全头
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-XSS-Protection "0" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
|
||||
server_tokens off;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 4. 前端 CSP Meta(双保险)
|
||||
|
||||
### 4.1 4 前端 `index.html` 加 meta CSP
|
||||
|
||||
**`frontend-admin/index.html`**:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
|
||||
<!-- CSP - 与 nginx 头保持一致 -->
|
||||
<meta http-equiv="Content-Security-Policy" content="
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://res.wx.qq.com;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob: https: http:;
|
||||
font-src 'self' data:;
|
||||
connect-src 'self' https://qyapi.weixin.qq.com wss://* https://*.servyou-it.com;
|
||||
media-src 'self' blob:;
|
||||
object-src 'none';
|
||||
frame-ancestors 'none';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
">
|
||||
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||
<meta http-equiv="Permissions-Policy" content="
|
||||
camera=(), microphone=(), geolocation=(), payment=()
|
||||
">
|
||||
|
||||
<title>IT 智能服务台 - 管理后台</title>
|
||||
</head>
|
||||
```
|
||||
|
||||
### 4.2 CSP 报告模式(先观察,再强制)
|
||||
|
||||
**第 1 步: Report-Only 模式(2 周)**:
|
||||
```nginx
|
||||
add_header Content-Security-Policy-Report-Only "..." always;
|
||||
```
|
||||
|
||||
**第 2 步: 收集违规报告**(发到 `/api/v1/csp-report`)
|
||||
|
||||
**第 3 步: 改 enforce 模式**:
|
||||
```nginx
|
||||
add_header Content-Security-Policy "..." always; # 不带 Report-Only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 5. 企微回调 IP 限制
|
||||
|
||||
### 5.1 企微回调 IP 段(从企微文档)
|
||||
|
||||
| 段 | 用途 |
|
||||
|---|---|
|
||||
| `101.226.103.0/24` | 企微上海 |
|
||||
| `101.226.108.0/24` | 企微上海 |
|
||||
| `140.207.54.0/24` | 企微上海 |
|
||||
| `140.207.61.0/24` | 企微深圳 |
|
||||
| `183.192.192.0/18` | 企微通用 |
|
||||
| `121.51.130.0/24` | 企微广州 |
|
||||
|
||||
**注意**: 实际范围可能变更,需查官方文档。
|
||||
|
||||
### 5.2 nginx 限制
|
||||
|
||||
```nginx
|
||||
location = /api/wecom/callback {
|
||||
# 只允许企微 IP 段
|
||||
allow 101.226.103.0/24;
|
||||
allow 101.226.108.0/24;
|
||||
allow 140.207.54.0/24;
|
||||
allow 140.207.61.0/24;
|
||||
allow 183.192.192.0/18;
|
||||
allow 121.51.130.0/24;
|
||||
|
||||
# 内网允许(开发)
|
||||
allow 127.0.0.1;
|
||||
allow 10.0.0.0/8;
|
||||
allow 172.16.0.0/12;
|
||||
allow 192.168.0.0/16;
|
||||
|
||||
deny all;
|
||||
|
||||
proxy_pass http://backend_api/api/wecom/callback;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 6. 速率限制(补充)
|
||||
|
||||
### 6.1 现有方案
|
||||
|
||||
`backend/app/main.py` 已用 `slowapi` 全局限流,默认 60/分钟。
|
||||
|
||||
### 6.2 精细化建议
|
||||
|
||||
| 路径 | 限制 | 理由 |
|
||||
|---|---|---|
|
||||
| `/api/v1/auth/login` | 5/分钟/IP | 防爆破 |
|
||||
| `/api/v1/auth/otp` | 3/分钟/IP | 防 OTP 暴力 |
|
||||
| `/api/v1/conversations` | 60/分钟/agent | 正常业务 |
|
||||
| `/api/v1/messages` | 120/分钟/agent | 消息多 |
|
||||
| `/api/wecom/callback` | 不限 | 企微回调 |
|
||||
|
||||
**配置示例**:
|
||||
```python
|
||||
# backend/app/main.py
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
@app.post("/api/v1/auth/login")
|
||||
@limiter.limit("5/minute")
|
||||
async def login(...):
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 7. 测试与验证
|
||||
|
||||
### 7.1 安全 Header 在线检测
|
||||
|
||||
```bash
|
||||
# Mozilla Observatory
|
||||
https://observatory.mozilla.org/analyze/itsupport.servyou.com.cn
|
||||
|
||||
# Security Headers
|
||||
https://securityheaders.com/?q=itsupport.servyou.com.cn
|
||||
|
||||
# SSL Labs(SSL 评估)
|
||||
https://www.ssllabs.com/ssltest/analyze.html?d=itsupport.servyou.com.cn
|
||||
```
|
||||
|
||||
### 7.2 CORS 自动化测试
|
||||
|
||||
**`scripts/cors-test.sh`**(新建):
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# CORS 自动化测试
|
||||
set -e
|
||||
|
||||
API="http://localhost:8000"
|
||||
ORIGIN="http://localhost:5173"
|
||||
|
||||
echo "=== 1. 预检请求 ==="
|
||||
curl -s -I -X OPTIONS \
|
||||
-H "Origin: $ORIGIN" \
|
||||
-H "Access-Control-Request-Method: POST" \
|
||||
-H "Access-Control-Request-Headers: Authorization" \
|
||||
"$API/api/v1/auth/login" | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== 2. 实际请求 ==="
|
||||
curl -s -I -H "Origin: $ORIGIN" "$API/api/v1/health" | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== 3. 期望 ==="
|
||||
echo "Access-Control-Allow-Origin: $ORIGIN"
|
||||
echo "Access-Control-Allow-Credentials: true"
|
||||
```
|
||||
|
||||
### 7.3 安全 Header 验证
|
||||
|
||||
**`scripts/security-headers-test.sh`**(新建):
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 安全 Header 验证
|
||||
|
||||
URL="${1:-http://localhost}"
|
||||
|
||||
echo "=== 检查安全头 ==="
|
||||
HEADERS=$(curl -sI "$URL/")
|
||||
|
||||
check_header() {
|
||||
local header=$1
|
||||
local expected=$2
|
||||
if echo "$HEADERS" | grep -qi "^$header:"; then
|
||||
echo "✅ $header: $(echo "$HEADERS" | grep -i "^$header:" | cut -d':' -f2- | xargs)"
|
||||
else
|
||||
echo "❌ $header: 缺失"
|
||||
fi
|
||||
}
|
||||
|
||||
check_header "Strict-Transport-Security"
|
||||
check_header "X-Content-Type-Options" "nosniff"
|
||||
check_header "X-Frame-Options"
|
||||
check_header "Content-Security-Policy"
|
||||
check_header "Referrer-Policy"
|
||||
check_header "Permissions-Policy"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 8. 实施路径
|
||||
|
||||
### 8.1 立即(本次跑批)
|
||||
|
||||
- [x] 审计报告写完(本文件)
|
||||
- [ ] 更新 `nginx.conf` + `nginx-nas.conf` 加 HSTS/CSP/Permissions
|
||||
- [ ] 加 `server_tokens off`
|
||||
- [ ] 4 前端 `index.html` 加 CSP meta
|
||||
|
||||
### 8.2 下周
|
||||
|
||||
- [ ] 改 CORS 精细化(分环境)
|
||||
- [ ] 企微回调 IP 白名单
|
||||
- [ ] 加 `/api/v1/csp-report` 端点
|
||||
- [ ] 跑 `cors-test.sh` 验证
|
||||
|
||||
### 8.3 季度
|
||||
|
||||
- [ ] 提交 https://hstspreload.org/(HSTS 预加载)
|
||||
- [ ] 跑 Mozilla Observatory(A+ 目标)
|
||||
- [ ] 跑 Security Headers(A 目标)
|
||||
|
||||
---
|
||||
|
||||
## 📌 9. 关联文档
|
||||
|
||||
- [[风险跟踪表]] M-3(无统一错误码)/ H-4(WS token)
|
||||
- [[后端架构]] §5 错误处理 / §4 中间件
|
||||
- [[外部系统集成]] §1-4(企微凭据)
|
||||
- [[健康检查+错误码+日志结构化]] - trace_id(配合 CSP 报告)
|
||||
|
||||
---
|
||||
|
||||
*本审计是 2026-06-15 Claude 满载跑批产出,待评审*
|
||||
Reference in New Issue
Block a user