341 lines
12 KiB
Markdown
341 lines
12 KiB
Markdown
|
|
# 技术方案 - REQ-通用-004 敏感词检测 v1.1.1(鉴权安全补漏)
|
|||
|
|
|
|||
|
|
> **版本**: v1.1.1(v1.1 的安全补丁 / PATCH 级别)
|
|||
|
|
> **日期**: 2026-08-05
|
|||
|
|
> **REQ编号**: REQ-通用-004
|
|||
|
|
> **状态**: [待评审] → 待部署
|
|||
|
|
> **作者**: 宋献 / Duckula
|
|||
|
|
> **关联PRD**: `docs/01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.1.1.md`
|
|||
|
|
> **关联缺陷单**: `docs/03-测试文档/05-缺陷单/BUG-通用-004-敏感词API无鉴权-001.md`
|
|||
|
|
> **关联任务说明书**: `docs/07-项目管理/任务说明书/任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md`
|
|||
|
|
> **关联整改记录**: `docs/04-运维文档/部署运维/00-文档规范化整改记录.md`(#5 整改)
|
|||
|
|
> **前置版本**: `技术方案-REQ-通用-004-敏感词检测-v1.0.archive.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 目录
|
|||
|
|
|
|||
|
|
1. [设计概述](#1-设计概述)
|
|||
|
|
2. [根因定位](#2-根因定位)
|
|||
|
|
3. [修复方案](#3-修复方案)
|
|||
|
|
4. [代码变更清单](#4-代码变更清单)
|
|||
|
|
5. [测试策略](#5-测试策略)
|
|||
|
|
6. [部署与回滚](#6-部署与回滚)
|
|||
|
|
7. [风险与豁免](#7-风险与豁免)
|
|||
|
|
8. [关联文档](#8-关联文档)
|
|||
|
|
9. [变更日志](#9-变更日志)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 设计概述
|
|||
|
|
|
|||
|
|
### 1.1 背景
|
|||
|
|
|
|||
|
|
v1.0(2026-07-28)已规定所有 admin 命名空间下的端点需要 `admin` 角色;v1.1(2026-07-28)落地了 13 个 admin 端点(`sensitive_words` / `privacy_patterns` / `moderation_logs` / `moderation_config`),但实施时 `src/backend/app/api/admin/sensitive_words.py` 的 `APIRouter` 创建时**未挂 `dependencies=[Depends(require_admin)]`**,13 端点全部无鉴权。
|
|||
|
|
|
|||
|
|
v1.1.1 作为 PATCH 级别的安全补漏,**仅做鉴权修复**,不改任何业务逻辑、API 契约、数据库 schema。
|
|||
|
|
|
|||
|
|
### 1.2 设计目标
|
|||
|
|
|
|||
|
|
| 目标 | 描述 |
|
|||
|
|
|---|---|
|
|||
|
|
| **G1 一行全覆盖** | 修改 APIRouter 配置 + 一行 import,13 端点同时获得 require_admin 依赖 |
|
|||
|
|
| **G2 与现有模式一致** | 复用 `app/api/admin_api.py:50` 已定义的 `require_admin` 函数(不重复实现) |
|
|||
|
|
| **G3 行为对齐** | 与 `admin_api.py` 已加鉴权的同类端点行为完全一致:非 admin 返回 403 + AppException(1004, "无管理权限") |
|
|||
|
|
| **G4 零业务影响** | admin 用户的 13 端点调用全部仍返回 200,业务流程不受影响 |
|
|||
|
|
| **G5 与 v1.2 解耦** | 不动 AI 工作流 / Dify 集成 / service 层逻辑;可独立部署 |
|
|||
|
|
|
|||
|
|
### 1.3 不做
|
|||
|
|
|
|||
|
|
- 不重构 service 层(`SensitiveWordService`)
|
|||
|
|
- 不引入新依赖
|
|||
|
|
- 不修改任何端点的请求/响应 schema
|
|||
|
|
- 不修改数据库 schema
|
|||
|
|
- 不加 operator_id 审计字段(列入 v1.1.2 或 v1.2)
|
|||
|
|
- 不加正则复杂度限制(列入 v1.1.2)
|
|||
|
|
- 不影响 v1.2 AI 化草案
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 根因定位
|
|||
|
|
|
|||
|
|
### 2.1 三层根因
|
|||
|
|
|
|||
|
|
#### 2.1.1 实现层(直接原因)
|
|||
|
|
|
|||
|
|
`sensitive_words.py:44`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# ❌ 当前实现(v1.1 落地时漏写)
|
|||
|
|
router = APIRouter(prefix="/admin", tags=["敏感词管理(v1.1)"])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
应写为:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# ✅ v1.1.1 修复后
|
|||
|
|
from app.api.admin_api import require_admin # 第 1 处变更:顶部 imports
|
|||
|
|
|
|||
|
|
router = APIRouter(
|
|||
|
|
prefix="/admin",
|
|||
|
|
tags=["敏感词管理(v1.1)"],
|
|||
|
|
dependencies=[Depends(require_admin)], # 第 2 处变更:router 配置
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.1.2 规范层(间接原因)
|
|||
|
|
|
|||
|
|
技术方案 v1.0 §6.3 "v1.1 计划路由层" 表格中 11 个端点已**明确列出"权限:admin"列**:
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 说明 | 权限 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| `/api/admin/sensitive-words` | GET | 列表查询 | **admin** |
|
|||
|
|
| `/api/admin/sensitive-words` | POST | 添加 | **admin** |
|
|||
|
|
| ... | ... | ... | **admin** |
|
|||
|
|
| `/api/admin/moderation-logs` | GET | 审计日志查询 | **admin** |
|
|||
|
|
| `/api/admin/moderation-stats` | GET | 命中统计 | **admin** |
|
|||
|
|
|
|||
|
|
但 v1.1 实施时未对照此规范逐项实现。
|
|||
|
|
|
|||
|
|
#### 2.1.3 流程层(系统原因)
|
|||
|
|
|
|||
|
|
任务说明书 v1.1 §5"输出成果要求"未列"鉴权维度验收"。TC-通用-004 31 条用例全在功能维度,无安全维度。
|
|||
|
|
|
|||
|
|
### 2.2 同类对照(不应发生但确实发生了)
|
|||
|
|
|
|||
|
|
| 文件 | 鉴权声明 | 端点数 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `admin_api.py` | ✅ 每端点 `Depends(require_admin)` | 30+ |
|
|||
|
|
| `admin_roles.py` | ✅ 已加 | 5 |
|
|||
|
|
| `admin_users.py` | ✅ 已加 | 8 |
|
|||
|
|
| `welcome.py` | ✅ 已加 | 3 |
|
|||
|
|
| `quiz_admin.py` | ✅ 已加 | 5 |
|
|||
|
|
| `troubleshooting_templates.py` | ✅ 已加 | 4 |
|
|||
|
|
| **`admin/sensitive_words.py`** | ❌ **0** | **13** |
|
|||
|
|
|
|||
|
|
### 2.3 鉴权依赖来源
|
|||
|
|
|
|||
|
|
`app/api/admin_api.py:50-66`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def require_admin(
|
|||
|
|
agent: Agent = Depends(get_current_agent),
|
|||
|
|
) -> Agent:
|
|||
|
|
"""管理后台权限校验:仅 role='admin' 可访问。"""
|
|||
|
|
if agent.role != "admin":
|
|||
|
|
raise AppException(1004, "无管理权限")
|
|||
|
|
return agent
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
可直接复用,**不重复实现**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 修复方案
|
|||
|
|
|
|||
|
|
### 3.1 修改点(共 2 处)
|
|||
|
|
|
|||
|
|
#### 3.1.1 顶部 imports(L33-41 后追加)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from app.api.admin_api import require_admin # v1.1.1 鉴权补漏
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.1.2 APIRouter 配置(L44 替换)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
router = APIRouter(
|
|||
|
|
prefix="/admin",
|
|||
|
|
tags=["敏感词管理(v1.1)"],
|
|||
|
|
dependencies=[Depends(require_admin)], # v1.1.1 鉴权补漏
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 行为变化
|
|||
|
|
|
|||
|
|
| 调用方 | v1.1 行为 | v1.1.1 行为 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 无 Authorization | 200 OK(裸奔) | 401 Unauthorized |
|
|||
|
|
| 普通坐席 token | 200 OK(裸奔) | 403 + `code:1004 无管理权限` |
|
|||
|
|
| 管理员 token | 200 OK | 200 OK(不变) |
|
|||
|
|
| 重载/正则测试等写操作 | 任意 token 可触发 | 仅 admin 可触发 |
|
|||
|
|
|
|||
|
|
### 3.3 鉴权链路(修复后)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
HTTP Request
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
[1] Depends(require_admin)
|
|||
|
|
│
|
|||
|
|
├──▶ Depends(get_current_agent) ─── 解析 JWT ───▶ 401 (token 无效)
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
[2] if agent.role != "admin": raise AppException(1004, "无管理权限")
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
[3] 进入 sensitive_words endpoint handler
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
[4] handler 返回 success_response(...)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 代码变更清单
|
|||
|
|
|
|||
|
|
| 文件 | 变更类型 | 行号 | 内容 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `src/backend/app/api/admin/sensitive_words.py` | 修改 | 顶部 imports | 新增 `from app.api.admin_api import require_admin` |
|
|||
|
|
| `src/backend/app/api/admin/sensitive_words.py` | 修改 | L44 | APIRouter 加 `dependencies=[Depends(require_admin)]` |
|
|||
|
|
| `src/backend/tests/test_sensitive_words_auth.py` | 新增 | - | 6 条鉴权测试用例 |
|
|||
|
|
|
|||
|
|
无 DB 迁移、无 frontend 改动、无 nginx 改动、无 docker-compose 改动。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 测试策略
|
|||
|
|
|
|||
|
|
### 5.1 新增自动化测试
|
|||
|
|
|
|||
|
|
`src/backend/tests/test_sensitive_words_auth.py`:
|
|||
|
|
|
|||
|
|
| 用例 | 输入 | 预期 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| test_no_token_returns_401 | 无 Authorization | 401 |
|
|||
|
|
| test_invalid_token_returns_401 | Bearer "invalid" | 401 |
|
|||
|
|
| test_agent_role_returns_403 | 普通坐席 token | 403 + `code:1004` |
|
|||
|
|
| test_admin_role_returns_200_list | admin GET /sensitive-words | 200 |
|
|||
|
|
| test_admin_role_returns_200_reload | admin POST /sensitive-words/reload | 200 |
|
|||
|
|
| test_admin_role_returns_200_pattern_test | admin POST /privacy-patterns/{id}/test | 200 |
|
|||
|
|
|
|||
|
|
### 5.2 容器内端到端验证
|
|||
|
|
|
|||
|
|
按 `deploy-troubleshoot` 铁律:**宣布修复前必须端到端验证**。
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1) 进入容器 grep 验证源码
|
|||
|
|
docker compose exec backend grep -n "require_admin" app/api/admin/sensitive_words.py
|
|||
|
|
# 预期输出:两行(import + dependencies)
|
|||
|
|
|
|||
|
|
# 2) 普通坐席 token curl 13 端点(应全部 403)
|
|||
|
|
TOKEN_AGENT="<普通坐席 token>"
|
|||
|
|
for endpoint in \
|
|||
|
|
"GET /api/admin/sensitive-words" \
|
|||
|
|
"POST /api/admin/sensitive-words/test" \
|
|||
|
|
"POST /api/admin/sensitive-words/reload" \
|
|||
|
|
"GET /api/admin/privacy-patterns" \
|
|||
|
|
"POST /api/admin/privacy-patterns/1/test" \
|
|||
|
|
"GET /api/admin/moderation-logs" \
|
|||
|
|
"GET /api/admin/moderation-logs/stats" \
|
|||
|
|
"GET /api/admin/moderation-config"; do
|
|||
|
|
METHOD=$(echo $endpoint | awk '{print $1}')
|
|||
|
|
PATH_=$(echo $endpoint | awk '{print $2}')
|
|||
|
|
curl -sS -X $METHOD "http://localhost:8000$PATH_" \
|
|||
|
|
-H "Authorization: Bearer $TOKEN_AGENT" -w "\nHTTP %{http_code}\n"
|
|||
|
|
done
|
|||
|
|
# 预期:全部 HTTP 403 + code:1004
|
|||
|
|
|
|||
|
|
# 3) admin token curl 13 端点(应全部 200)
|
|||
|
|
TOKEN_ADMIN="<admin token>"
|
|||
|
|
# 同上循环,预期全部 HTTP 200
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 回归测试
|
|||
|
|
|
|||
|
|
- TC-通用-004 既有 23/31 通过用例不变
|
|||
|
|
- 内容审核业务(坐席发消息审核)流程不变
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 部署与回滚
|
|||
|
|
|
|||
|
|
### 6.1 部署步骤(按 jumpserver-V2 铁律)
|
|||
|
|
|
|||
|
|
1. **本地构建产物**:无需前端构建;后端无新依赖,`pip install` 不变
|
|||
|
|
2. **本地源码变更同步到 ASCII 构建路径**(按 §11.7 多路径铁律):
|
|||
|
|
- 中文路径:`D:\资料\03-项目开发\wecom_it_smart_desk\src\backend\app\api\admin\sensitive_words.py`
|
|||
|
|
- ASCII 路径:`D:\dev\wecom\src\backend\app\api\admin\sensitive_words.py`
|
|||
|
|
- **两边都改**
|
|||
|
|
3. **上传到生产**:
|
|||
|
|
```bash
|
|||
|
|
# 走 psftp 通道(按部署铁律)
|
|||
|
|
psftp> put sensitive_words.py /<资产>/<系统用户>/
|
|||
|
|
```
|
|||
|
|
4. **容器内操作**:
|
|||
|
|
```bash
|
|||
|
|
# 远程禁用 $(),按铁律
|
|||
|
|
docker cp /tmp/sensitive_words.py wecom_it_backend:/app/app/api/admin/sensitive_words.py
|
|||
|
|
docker compose restart backend
|
|||
|
|
```
|
|||
|
|
5. **验证**:跑 §5.2 端到端 curl
|
|||
|
|
|
|||
|
|
### 6.2 回滚预案(30 min 可逆)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1) 恢复源码(git checkout HEAD~1 或手动删除 dependencies 那行)
|
|||
|
|
docker cp /tmp/sensitive_words.py.bak wecom_it_backend:/app/app/api/admin/sensitive_words.py
|
|||
|
|
# 2) 重启
|
|||
|
|
docker compose restart backend
|
|||
|
|
# 3) 验证:admin 仍可访问 13 端点(恢复裸奔状态)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
回滚代价:再次陷入 13 端点裸奔状态(但仅 30 min 内应急,业务可接受)。
|
|||
|
|
|
|||
|
|
### 6.3 灰度开关
|
|||
|
|
|
|||
|
|
不需要灰度。修复本身就是"从危险→安全"的方向,**无需开关**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 风险与豁免
|
|||
|
|
|
|||
|
|
| 风险 | 等级 | 缓解 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| admin 误锁(token 过期) | 🟢 低 | 401 不是 403,前端可正常重登录 |
|
|||
|
|
| 修复不彻底(漏掉新端点) | 🟢 低 | 本次仅 1 个文件 13 端点,APIRouter `dependencies=` 一行全覆盖 |
|
|||
|
|
| 性能损耗 | 🟢 低 | `get_current_agent` 缓存 JWT 解析;< 1ms 开销 |
|
|||
|
|
| require_admin 函数被改名 | 🟢 低 | `admin_api.py:50` 已是稳定函数,6 个同类文件已引用 |
|
|||
|
|
| 与 v1.2 部署冲突 | 🟢 低 | 独立部署,CI 可串行 |
|
|||
|
|
| 修复后没跑端到端 | 🟠 高 | 强制走 §5.2 容器内 curl 验证,不信源码 grep |
|
|||
|
|
|
|||
|
|
### 7.1 豁免清单
|
|||
|
|
|
|||
|
|
| 端点 | 豁免原因 |
|
|||
|
|
|---|---|
|
|||
|
|
| 无(13 端点全部要求 admin) | - |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 关联文档
|
|||
|
|
|
|||
|
|
| 文档 | 位置 | 关联点 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 前置技术方案 v1.0 | `02-技术文档/技术架构/技术方案-REQ-通用-004-敏感词检测-v1.0.archive.md` | §6.3 路由层表格已规定 admin |
|
|||
|
|
| 关联 PRD v1.1.1 | `01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.1.1.md` | 需求来源 |
|
|||
|
|
| 关联缺陷单 | `03-测试文档/05-缺陷单/BUG-通用-004-敏感词API无鉴权-001.md` | 触发 |
|
|||
|
|
| 关联任务说明书 v1.1.1 | `07-项目管理/任务说明书/任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md` | 任务清单 |
|
|||
|
|
| 测试用例 | `03-测试文档/03-功能测试用例/TC-通用-004-敏感词检测.md` | 加 §10 鉴权章节 |
|
|||
|
|
| 整改记录 | `04-运维文档/部署运维/00-文档规范化整改记录.md` | #5 整改 |
|
|||
|
|
| 鉴权依赖定义 | `src/backend/app/api/admin_api.py:50-66` | 复用 |
|
|||
|
|
| 同类已加鉴权文件 | `admin_api.py` / `admin_roles.py` / `admin_users.py` / `welcome.py` / `quiz_admin.py` / `troubleshooting_templates.py` | 对照实现 |
|
|||
|
|
| 项目规范 | `docs/00-产品开发流程与文档管理规范.md` v1.9 | § 4.3 文档更新时机 + § 11 整改实践 |
|
|||
|
|
| 多路径铁律 | `docs/00-产品开发流程与文档管理规范.md` § 11.7 | 中文 + ASCII 双改 |
|
|||
|
|
| 部署铁律 | `docs/00-产品开发流程与文档管理规范.md` § 11.5-11.8 | 验证三证据链 |
|
|||
|
|
| 部署排查手册 | `docs/04-运维文档/部署运维/00-标准故障排查手册.md` | 容器内操作参考 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 变更日志
|
|||
|
|
|
|||
|
|
| 版本 | 日期 | 变更 | 变更人 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| v1.0 | 2026-07-28 | 首次整理:v0.7.1 上线内容回溯为正式技术方案 | 宋献 |
|
|||
|
|
| v1.1 | 2026-07-28 | DB化 + 后台 UI + 13 端点实施(**实施时漏加鉴权**,BUG-通用-004) | 宋献 |
|
|||
|
|
| **v1.1.1** | **2026-08-05** | **PATCH 级别安全补漏:APIRouter 加 `dependencies=[Depends(require_admin)]`,13 端点全覆盖** | **宋献 / Duckula** |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
> **关键决策记录**:
|
|||
|
|
> - **2026-08-05**:v1.1.1 作为 PATCH 级别安全补丁;不阻塞 v1.2 AI 化草案;operator_id 审计 / 正则复杂度限制列入 v1.1.2 或 v1.2 跟进
|
|||
|
|
> - **强制规范沉淀**:任何 `APIRouter(prefix="/admin", ...)` **必须**声明 `dependencies=[Depends(require_admin)]`,无显式豁免不得省略
|