v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化

This commit is contained in:
Simon
2026-07-17 23:08:59 +08:00
parent 5a77a89ab1
commit 3ed86d5fb3
181 changed files with 19738 additions and 2655 deletions
@@ -1,9 +1,16 @@
# 00 · 标准故障排查手册
> **版本**: v1.4 | **日期**: 2026-07-10 | **维护人**: 宋献 / 助理
> **版本**: v1.9 | **日期**: 2026-07-16 | **维护人**: 宋献 / 助理
> **定位**: 所有故障排查前**首先查看本手册**。
> **最新**: 方案 C(卷挂载)已上线,§1.4 更新为 volume 挂载验证 + CASE-20260710-02 标注根因已消除 + 错误码速查更新
> **最新**: 新增 CASE-20260716-01(员工端 /h5/ 404 — nginx 反向代理配置错误)
| v1.6 | 2026-07-14 | 新增 CASE-20260714-01(前端 bind mount 未生效导致 403+ 错误码速查更新 |
| v1.7 | 2026-07-14 | 新增 CASE-20260714-02(员工端 /h5/ 404 — nginx 配置错误)|
| v1.9 | 2026-07-16 | 新增 CASE-20260716-01(员工端 /h5/ 404 — nginx 反向代理配置错误)|
| v1.8 | 2026-07-15 | 新增 CASE-20260715-01(员工端审批模板ID不正确 — 企微审批模板失效)|
| v1.7 | 2026-07-14 | 新增 CASE-20260714-01(前端 bind mount 未生效导致 403+ 错误码速查更新 |
| v1.6 | 2026-07-14 | 新增 CASE-20260714-02(员工端 /h5/ 404 — nginx 配置错误)|
| v1.5 | 2026-07-13 | 新增 CASE-20260713-01nginx rewrite 不全导致 502+ CASE-20260713-02@require_role 装饰器参数缺失) |
| v1.4 | 2026-07-10 | 方案 C 上线:§1.4 改为 volume 挂载验证 + 错误码速查更新 + CASE-20260710-02 根因已消除标注 |
> **前置阅读**: [运维手册(部署/回滚/备份/应急)](../01-项目总览/01-智能IT服务系统运维手册-20260704.md) · [SOP-04 应急响应](../10-项目管理/SOPs-标准流程/SOP-04-应急响应.md)
@@ -136,16 +143,41 @@ python jms_ops.py exec \
> ⚠️ 代码更新后 `docker compose restart` 即可(15-30 秒)。仅当 `requirements.txt` 有变化时才需要 `docker compose build`。
#### 前端部署检查清单(bind mount 模式)
**触发条件**:任何前端文件更新(`frontend-*/dist/` 目录变更)。
```bash
# 1. 验证宿主机源目录有文件
ls -la /opt/wecom-it-desk/frontend-agent/dist/ # 坐席端
ls -la /opt/wecom-it-desk/frontend-h5/dist/ # H5 端
# 2. 验证容器内挂载成功(关键!)
docker exec wecom_it_nginx ls /usr/share/nginx/html/itagent/ # 坐席端
docker exec wecom_it_nginx ls /usr/share/nginx/html/h5/ # H5 端
# 3. 如容器内为空,强制重建 nginx 容器
cd /opt/wecom-it-desk
docker compose stop nginx
docker compose rm -f nginx
docker compose up -d nginx
# 4. 验证外部可访问
curl -sI https://itsupport.servyou.com.cn/itagent/ | head -3
```
> ⚠️ **bind mount 有时不生效**:即使 docker-compose.yml 配置正确,挂载也可能失效。前端部署后务必验证容器内文件存在(步骤2),否则会 403。
---
## 2 常见错误码速查(E5xx
| 错误码 | 含义 | 首选排查 |
|--------|------|---------|
| **E500** | 后端未捕获异常 / 缺列 / 缺依赖 | `docker compose logs backend --tail=200 \| grep -i error`;查数据库缺列 / 缺 Python 依赖 |
| **E502** | nginx 连不到后端 | 后端容器 `unhealthy``docker logs wecom_it_backend`是否缺 `PYTHONPATH=/app` |
| **E500** | 后端未捕获异常 / 缺列 / 缺依赖 / 装饰器参数不匹配 | `docker compose logs backend --tail=200 \| grep -i error`;查数据库缺列 / 缺 Python 依赖;检查 `@require_role` 等装饰器是否与函数签名不匹配(见 CASE-20260713-02 |
| **E502** | nginx 连不到后端 / 路由不匹配 | 后端容器 `unhealthy``docker logs wecom_it_backend`检查 nginx rewrite 是否正确剥离 `/api/` 前缀(见 CASE-20260713-01 |
| **E503** | 服务过载 / 维护 | `docker stats``docker inspect ... Health` |
| **E403** | IP 白名单 / 无权限 | `grep allow /opt/wecom-it-desk/nginx/nginx.conf`admin 角色不足 |
| **E403** | IP 白名单 / 无权限 / 前端挂载未生效 | `grep allow /opt/wecom-it-desk/nginx/nginx.conf`admin 角色不足;前端部署后检查 `docker exec wecom_it_nginx ls <挂载路径>` 确认容器内文件存在(见 CASE-20260714-01 |
| **E422** | 请求体校验失败(Pydantic)| 确认必填字段齐全(如登录需 `user_id`+`name`|
| **网络失败 / 连接挂起** | 依赖不可达(最常见 Redis 配置错)| 见 §1.2 / CASE-20260707-01 |
| **页面 200 但 JS 不执行** | CSP 头 `script-src``'unsafe-inline'`,内联 `<script>` 被浏览器静默拦截 | `curl -ksI <URL> \| grep content-security`;检查 `script-src` 是否含 `'unsafe-inline'`(见 CASE-20260710-01|
@@ -201,6 +233,153 @@ docker logs wecom_it_backend | grep -i websocket
## 4 案例库(倒序,编号 CASE-YYYYMMDD-序号)
### CASE-20260716-01 · 员工端 /h5/ 404 — nginx 反向代理配置错误 ⭐⭐
- **现象**:企微工作台 → IT智能服务 → `https://itsupport.servyou.com.cn/h5/` 返回 `{"detail":"Not Found"}`FastAPI 404
- **根因**nginx 配置中 `/h5/` 被错误配置为**反向代理**到后端 API(`proxy_pass http://backend_api/`),而不是静态文件服务。后端没有 `/h5/` 路由,返回 404
- **诊断**
1. `docker logs wecom_it_nginx | grep /h5/` → 显示后端返回 404
2. `docker exec wecom_it_nginx cat /etc/nginx/nginx.conf | grep -A5 'location /h5/'` → 发现 `proxy_pass http://backend_api/`
3. 确认 H5 静态文件已正确挂载:`docker exec wecom_it_nginx ls /usr/share/nginx/html/h5/`
- **修复**
1. **docker-compose.yml**:新增 H5 挂载到 `/h5/`
```
- ./frontend-h5/dist:/usr/share/nginx/html/h5:ro
```
2. **nginx/nginx.conf**:将 `/h5/` 从反向代理改为静态文件服务
```
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/index.html;
}
```
3. **部署**:上传配置后重建 nginx 容器
```
cd /opt/wecom-it-desk
docker compose stop nginx && docker compose rm -f nginx && docker compose up -d nginx
```
- **验证**
- 容器内 `curl -sI http://localhost/h5/` → HTTP 200
- 浏览器访问 `https://itsupport.servyou.com.cn/h5/` → 正常显示 H5 页面
- **⚠️ 教训**
1. **H5 前端是静态文件应用**nginx 应配置 `alias` 或 `root` 提供静态文件,而不是 `proxy_pass` 到后端
2. **这是第二次出现同类问题**:上次 CASE-20260714-02 修复后,今天再次出现,可能是上次修复未同步到服务器或配置被覆盖
3. **bind mount 有时不生效**:即使 docker-compose.yml 配置正确,挂载也可能失效。前端部署后务必验证容器内文件存在
4. **建议**:在 nginx 配置中添加注释说明 `/h5/` 是静态文件服务,避免未来误改
### CASE-20260715-01 · 员工端审批"获取审批流程失败,审批模板ID不正确" ⭐⭐
- **现象**:员工端H5点击"IT资产升级"、"设备申请"、"商业软件申请"等审批卡片时,提示"获取审批流程失败,审批模板ID不正确"。
- **根因**(两层代码都需要修复):
1. **前端**`RecommendCard.vue` 中 `APPROVAL_URL_MAP` 的 `asset_upgrade` 映射到企微审批模板 `Bs7ucTGs...`(已失效),`商业软件申请` 映射到企微审批模板 `3TmACf8D...`(也已失效),第298-299行硬编码 fallback 跳转到失效的企微审批URL
2. **后端**`backend/app/api/approval.py` 中 `APPROVAL_TEMPLATES` 字典的 `asset_upgrade` 和 `software_service` 同样映射到失效的企微审批模板(第81行和第115行)
- **诊断**
1. 浏览器F12查看网络请求,确认调用的是企微审批模板URL
2. 检查前端 `RecommendCard.vue` 和后端 `approval.py` 中的URL映射
3. 对比 `ApprovalCardModal.vue` 中的正确映射(已改用ITSM
- **修复**
1. **前端**`frontend-h5/src/components/assistant/RecommendCard.vue`
- 将 `asset_upgrade` 改为ITSM工单系统URL
- 将 `商业软件申请` 改为ITSM工单系统URL
- 修改 `handleActionClick()` 的fallback逻辑,移除硬编码的失效URL
- 构建:`npm run build` → 部署到 `/opt/wecom-it-desk/frontend-h5/dist/`
2. **后端**`backend/app/api/approval.py`
- 将 `asset_upgrade` 的 `url` 改为ITSM工单系统URL`location` 改为"运维平台"
- 将 `software_service` 的 `url` 改为ITSM工单系统URL`location` 改为"运维平台"
- 上传到服务器:`/opt/wecom-it-desk/app/api/approval.py`
- 重启后端:`docker compose restart backend`
3. 部署:`docker exec wecom_it_nginx nginx -s reload`
- **验证**:浏览器访问H5,点击审批卡片,跳转到ITSM工单系统而非企微审批(已失效模板)
- **⚠️ 教训**
1. **审批入口有两层**:左侧"企微-审批"入口数据来自后端API `/approval/links`,AI推荐卡片来自前端代码,两者都需要修复
2. **企微审批模板会失效**:审批模板在企微后台可能被删除或变更,前端不应硬编码模板ID
3. **ITSM是更稳定的方案**:ITSM工单系统URL更稳定,不受企微审批模板变更影响
4. **推荐做法**:所有审批类型统一使用ITSM工单系统,前后端都要同步修改
### CASE-20260714-01 · 坐席端 403 错误 — 前端 bind mount 未生效 ⭐⭐
- **现象**:坐席端 `/itagent/` 返回 403 Forbidden,浏览器控制台显示 `Failed to load resource: the server responded with a status of 403`。
- **根因**:前端文件虽然上传到服务器 `/opt/wecom-it-desk/frontend-agent/dist/`,但 nginx 容器的 bind mount 没有正确工作,容器内 `/usr/share/nginx/html/itagent/` 目录为空。
- **诊断**
1. `docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itagent/` → 容器内目录为空
2. `docker inspect wecom_it_nginx | grep itagent` → 挂载配置存在且正确(`Source: /opt/wecom-it-desk/frontend-agent/dist`
3. `ls -la /opt/wecom-it-desk/frontend-agent/dist/` → 宿主机源目录有文件
4. nginx 日志显示 `directory index of "/usr/share/nginx/html/itagent/" is forbidden`
- **修复**
```bash
# 重建 nginx 容器让 bind mount 生效
cd /opt/wecom-it-desk
docker compose stop nginx
docker compose rm -f nginx
docker compose up -d nginx
```
- **验证**`docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itagent/` → 容器内文件存在;外部访问 `/itagent/` → HTTP 200
- **⚠️ 教训**
1. **bind mount 有时不生效**:即使 docker-compose.yml 配置正确,挂载也可能失效。重建容器是常用解决方法。
2. **容器内目录为空但宿主机有文件 ≠ 挂载成功**:必须进入容器内验证,不能只看宿主机。
3. **前端部署后务必验证容器内文件**:用 `docker exec <容器> ls <挂载路径>` 确认。
4. **此问题可能复发**:任何前端部署(dist 目录更新)后,建议重建 nginx 容器确保挂载生效。
- **复发记录**
- 2026-07-16:再次发生,返回 500rewrite 循环 `internal redirection cycle while internally redirecting to "/itage"`),重建 nginx 容器修复
### CASE-20260714-02 · 员工端 /h5/ 404 — nginx 配置错误 ⭐⭐
- **现象**:员工端打开报错 `Failed to load resource: the server responded with a status of 404 (Not Found)`。
- **根因**nginx 配置中 `/h5/` 被错误配置为**反向代理**到后端(`proxy_pass http://backend_api/`),而不是静态文件服务。后端没有 `/h5/` 路由,导致返回 404。
- **诊断**
1. `docker logs wecom_it_nginx | grep /h5/` → 显示 `GET /h5/ HTTP/1.1" 404`
2. `docker exec wecom_it_nginx cat /etc/nginx/nginx.conf | grep -A10 'location /h5/'` → 发现 `proxy_pass http://backend_api/`
3. 确认静态文件已正确挂载:`docker exec wecom_it_nginx ls /usr/share/nginx/html/h5/`
- **修复**:修改 `nginx/nginx.conf`,将 `/h5/` 从反向代理改为静态文件服务:
```
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/index.html;
}
```
部署:`docker compose restart nginx`
- **验证**:浏览器访问 `https://itsupport.servyou.com.cn/h5/` → HTTP 200,显示"仅限企业微信访问"页面
- **⚠️ 教训**
1. **H5 前端是静态文件应用**nginx 应配置 `alias` 或 `root` 提供静态文件,而不是 `proxy_pass` 到后端。
2. **本地 nginx.conf 与服务器不同步**:发现问题后,检查本地配置是否已修复(本次修复已同步到本地 `nginx/nginx.conf`)。
3. **常见混淆**`/itdesk/` 和 `/h5/` 都指向 H5 应用,但只有 `/itdesk/` 是在企微工作台中配置的入口。两者都应配置为静态文件服务。
### CASE-20260713-02 · 坐席端 502 错误 — @require_role 装饰器参数缺失 ⭐⭐
- **现象**:坐席端 `/itagent/` 全部 API 返回 502,后端日志显示 `TypeError: list_agents() got an unexpected keyword argument 'current_user'`。
- **根因**`@require_role` 装饰器在调用被装饰函数时强制传递 `current_user` 参数,但部分使用该装饰器的函数未声明此参数。
- **诊断**`docker logs wecom_it_backend | grep -i error` 查看具体错误;定位缺少参数的函数。
- **修复**
1. `backend/app/api/agents.py` 第 425 行:添加 `current_user: UserInfo = Depends(get_current_user)`
2. `backend/app/api/otp.py` 第 348 行和第 389 行:同样添加 `current_user` 参数
3. 部署:`docker compose restart backend`
- **验证**:后端 `/health` → 200 OK`/agents` → 200 OK
- **⚠️ 教训**
1. **所有使用 `@require_role` 装饰器的函数都必须声明 `current_user` 参数**,装饰器会自动注入此参数。
2. 排查 502 错误时,先检查后端日志中的 `TypeError` 错误,可能是装饰器参数不匹配。
3. 建议在代码中添加类型注解和 lint 规则,提前发现此类问题。
### CASE-20260713-01 · 坐席端 502 错误 — nginx rewrite 规则不完整 ⭐⭐
- **现象**:部分 API(如 `/api/auth/qrcode`)返回 502,但 `/api/agents` 返回 200。
- **根因**nginx 配置只对 `/api/admin/` 路径做 rewrite 剥离前缀,其他路径(如 `/api/auth/`)未处理,导致后端收到 `/api/auth/qrcode` 而非 `/auth/qrcode`,路由不匹配返回 404。
- **诊断**
1. `docker logs wecom_it_nginx | grep 502` 查看哪些路径返回 502
2. `docker exec wecom_it_backend curl http://localhost:8000/auth/qrcode` 直接测试后端(绕过 nginx
3. 检查 nginx 配置 `location /api/` 是否正确 rewrite
- **修复**:修改 `nginx/nginx.conf`,对所有 `/api/` 路径统一做 rewrite
```
location /api/ {
# 剥离 /api/ 前缀,使后端收到 /auth/... 而非 /api/auth/...
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://backend_api;
...
}
```
部署:`docker restart wecom_it_nginx`
- **验证**:外部访问 `/api/auth/qrcode` → 200 OK
- **⚠️ 教训**
1. **nginx rewrite 规则必须覆盖所有需要剥离前缀的路径**,不能只处理特定路径(如 `/api/admin/`)。
2. 判定矩阵更新:`/xxx/ 200 但 /api/... 404` → 检查 nginx 是否正确 rewrite。
3. 部署新功能后,务必用真实浏览器测试所有关键路径,不能只测"看起来正常"的端点。
### CASE-20260710-02 · 坐席端/管理端登录"获取二维码失败"(Docker 镜像缺文件)⭐⭐
- **现象**:坐席端和管理端登录均报"获取二维码失败",浏览器控制台显示 `WebSocket connection failed` + `/api/auth/qrcode` 返回 404。
- **误判历程**
+34
View File
@@ -164,6 +164,40 @@ ping itsupport.servyou.com.cn
# CORS_ORIGINS=http://10.90.5.110
```
### 4.3 部署铁律(2026-07-17 新增,踩坑记录)
以下 3 条均为生产事故复盘得出的硬性规则,部署/变更时必须遵守:
#### 铁律 1backend 必须 `--workers 1`WS 推送单 worker 约束)
`ws_manager` 是进程内单例,AI 后台任务与员工 WebSocket 连接若落在不同 worker 进程,`broadcast_to_employees` 会**静默丢失约 50% 消息**。
```bash
# ❌ 危险:docker-compose-override.yml 会自动合并覆盖主文件!
# 若 override 中存在 --workers 2,主文件的 --workers 1 会被覆盖
docker compose -f docker-compose.yml -f docker-compose-override.yml config | grep workers
# 必须输出 1。如为 2,删除或修改 override 文件
```
#### 铁律 2:`.env` 变量不会自动传入容器,必须在 `environment:` 显式声明
`backend/.dockerignore` 排除了全部 `.env` 文件,生产容器配置 **100% 来自 docker-compose.yml 的 `environment:` 部分**。新增任何环境变量(尤其是 `DIFY_NATIVE_BASE_URL``DIFY_NATIVE_API_KEY`),必须:
```yaml
# docker-compose.yml
backend:
environment:
- DIFY_NATIVE_BASE_URL=${DIFY_NATIVE_BASE_URL:-}
- DIFY_NATIVE_API_KEY=${DIFY_NATIVE_API_KEY:-}
```
验证:容器内执行 `env | grep DIFY_NATIVE` 非空;后端日志出现「调用 Dify 原生 API」而非「回退到代理路径」。
(事故记录:2026-07-13 两次因未声明导致 P0 故障;`WECOM_SSO_CALLBACK_BASE` 未声明导致扫码登录崩溃)
#### 铁律 3Redis 密码含特殊字符必须 URL 编码
`REDIS_URL` 中密码含 `@ # !` 时未编码 → 解析出错误的 host → 连接挂起 → 登录 502。编码规则:`@→%40``#→%23``!→%21`。改密码时须同步更新 compose 中 `requirepass``REDIS_URL` 两处。
---
## 五、启动服务
@@ -0,0 +1,246 @@
# Token多IP异常检测 - 部署指南
> **任务ID**: 待分配
> **版本**: v1.0
> **关联文档**:
> - 技术设计-Token多IP异常检测.md
> - Token多IP异常检测测试用例.md
---
## 1. 部署概述
### 1.1 部署范围
| 组件 | 容器 | 说明 |
|------|------|------|
| 检测服务 | backend | Token异常检测定时任务 |
| Redis | redis | IP存储 |
| 告警通道 | 企微机器人 | 复用现有webhook |
### 1.2 部署方式
- **部署类型**: 增量部署(不涉及基础设施变更)
- **停机时间**: 无需停机(定时任务后台运行)
- **回滚**: 代码级别回滚
---
## 2. 部署前检查
### 2.1 环境检查
| 检查项 | 命令 | 预期结果 |
|--------|------|----------|
| Redis连接 | `docker exec backend redis-cli ping` | PONG |
| APScheduler状态 | 检查日志 | 定时任务启动成功 |
| webhook配置 | 检查环境变量 | CONTENT_AUDIT_WEBHOOK已设置 |
### 2.2 配置检查
确认以下环境变量已配置(如需自定义):
```bash
# 可选配置
TOKEN_ANOMALY_THRESHOLD=3 # 触发告警的IP数量阈值
TOKEN_ANOMALY_WINDOW=3600 # 时间窗口(秒)
TOKEN_ANOMALY_AUTO_DISABLE=false # 是否自动禁用Token
```
---
## 3. 部署步骤
### 3.1 步骤1:代码变更
**新增文件**
```
backend/app/tasks/token_anomaly_detection.py (新建)
```
**修改文件**
```
backend/app/services/token_service.py (增加record_token_ip方法)
backend/app/main.py (注册定时任务)
```
### 3.2 步骤2:配置变更
`.env` 或 docker-compose.yml 中添加(可选):
```bash
# 如需自定义阈值,在 .env 中添加
TOKEN_ANOMALY_THRESHOLD=3
TOKEN_ANOMALY_AUTO_DISABLE=false
```
### 3.3 步骤3:重启服务
```bash
# 重启backend容器(不中断其他服务)
docker compose restart backend
# 查看日志确认定时任务启动
docker logs backend --tail 50 | grep -i "token"
```
预期日志:
```
✅ Token多IP异常检测任务已启动(每60秒执行一次)
```
---
## 4. 部署后验证
### 4.1 功能验证
| 验证项 | 验证方法 | 预期结果 |
|--------|----------|----------|
| 定时任务运行 | 查看日志 | 每分钟执行一次 |
| IP记录 | 模拟API请求 | Redis中记录IP |
| 告警触发 | 构造3个IP的Token | 企微收到告警 |
### 4.2 冒烟测试
执行测试用例:
```bash
# 进入backend容器
docker exec -it backend bash
# 运行单元测试
pytest app/tests/test_token_anomaly.py -v
```
---
## 5. 监控与运维
### 5.1 日志位置
| 日志类型 | 路径 |
|----------|------|
| 应用日志 | `/app/logs/wecom-it-desk.log` |
| 定时任务日志 | 集成在应用日志中 |
### 5.2 监控指标
| 指标 | 说明 |
|------|------|
| token_anomaly_triggered_total | 触发告警次数 |
| token_anomaly_false_positive | 误报次数 |
### 5.3 运维命令
```bash
# 查看定时任务状态
docker exec backend python -c "from app.main import _scheduler; print(_scheduler.get_jobs())"
# 手动触发检测(调试)
docker exec backend python -c "
import asyncio
from app.tasks.token_anomaly_detection import detect_token_anomaly
asyncio.run(detect_token_anomaly())
"
```
---
## 6. 回滚方案
### 6.1 回滚步骤
```bash
# 1. 撤销代码变更
git checkout -- backend/app/services/token_service.py
git checkout -- backend/app/main.py
git rm backend/app/tasks/token_anomaly_detection.py
# 2. 重启服务
docker compose restart backend
```
### 6.2 数据清理
```bash
# 清理Redis中的检测数据(可选,1小时后自动过期)
docker exec backend redis-cli KEYS "token_ips:*" | xargs redis-cli DEL
```
---
## 7. 部署清单
| 序号 | 步骤 | 执行人 | 检查人 | 日期 |
|------|------|--------|--------|------|
| 1 | 代码变更 | 开发 | | |
| 2 | 配置检查 | 开发 | | |
| 3 | 重启服务 | 运维 | | |
| 4 | 功能验证 | 测试 | | |
| 5 | 冒烟测试 | 测试 | | |
| 6 | 监控确认 | 运维 | | |
---
## 8. 附录
### 8.1 相关文件
| 文件路径 | 说明 |
|----------|------|
| `backend/app/tasks/token_anomaly_detection.py` | 检测任务 |
| `backend/app/services/token_service.py` | Token服务 |
| `docs/09-部署运维/技术设计-Token多IP异常检测.md` | 技术设计 |
### 8.2 环境变量参考
| 变量 | 必需 | 默认值 | 说明 |
|------|------|--------|------|
| CONTENT_AUDIT_WEBHOOK | 是 | - | 企微机器人webhook |
| TOKEN_ANOMALY_THRESHOLD | 否 | 3 | 告警阈值 |
| TOKEN_ANOMALY_AUTO_DISABLE | 否 | false | 自动禁用 |
---
## 9. 部署测试结果(2026-07-14
### 9.1 部署清单
| 序号 | 步骤 | 执行人 | 检查人 | 日期 | 结果 |
|------|------|--------|--------|------|------|
| 1 | 代码变更 | 开发 | | 2026-07-14 | ✅ 完成 |
| 2 | 配置检查 | 开发 | | 2026-07-14 | ✅ 完成 |
| 3 | 重启服务 | 运维 | | 2026-07-14 | ✅ 完成 |
| 4 | 功能验证 | 测试 | | 2026-07-14 | ✅ 通过 |
| 5 | 冒烟测试 | 测试 | | 2026-07-14 | ✅ 通过 |
| 6 | 监控确认 | 运维 | | 2026-07-14 | ✅ 完成 |
### 9.2 功能测试结果
| 用例ID | 测试项 | 预期结果 | 实际结果 | 状态 |
|--------|--------|-----------|-----------|------|
| T001 | 创建测试数据 | Redis中记录3个IP | ✅ token_ip:ed733a26239b8f18 = 3个IP | ✅ 通过 |
| T002 | 触发检测 | 检测到异常并告警 | ✅ 检测到 token_hash=ed733a26239b8f18, ip_count=3 | ✅ 通过 |
| T003 | 企微告警 | 发送markdown告警 | ✅ Token异常告警已发送: 1条 | ✅ 通过 |
### 9.3 验证日志
```
检测到Token异常使用: token_hash=ed733a26239b8f18, ip_count=3, ips=['192.168.1.100', '192.168.1.102', '192.168.1.101']
HTTP Request: POST https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=09120612-9d19-4f93-bd00-bfbaef548dde "HTTP/1.1 2
Token异常告警已发送: 1 条
Token异常检测完成: 检测到 1 个异常
```
### 9.4 发现问题与修复
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 告警未发送 | 容器内.env文件路径错误 | 修改config.py的env_file为"/app/app/.env",并复制.env到挂载目录 |
| webhook未配置 | .env未同步到容器 | 复制.env到/app/app/.env |
---
> **编制人**: 威胁检测工程师
> **日期**: 2026-07-14
> **审核人**: 待定
@@ -0,0 +1,291 @@
# Token多IP异常检测 - 技术设计文档
> **任务ID**: 待分配
> **模块**: 威胁检测
> **优先级**: P1
> **ATT&CK**: T1078 (有效账户), T1552 (非安全凭据)
---
## 1. 需求概述
### 1.1 业务背景
当前系统已废弃密码登录,仅支持企微OAuth2/扫码登录。Token是用户身份的唯一凭证,当Token被泄露后,攻击者可能从不同IP使用同一Token访问系统。本功能旨在检测此类异常行为。
### 1.2 功能目标
- 记录每个Token使用的IP地址
- 检测同一Token在短时间内被多个IP使用的情况
- 触发告警通知安全管理员
- 可选:自动禁用异常Token
---
## 2. 技术方案
### 2.1 架构设计
```
┌─────────────────────────────────────────────────────────┐
│ 检测流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 用户API请求 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ record_token_ip │ ← 每次请求记录IP │
│ │ (埋点) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Redis Set │ ← token_ips:{hash} │
│ │ IP集合(1h TTL) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ 定时任务(每分钟) │
│ ┌─────────────────┐ │
│ │ detect_anomaly │ ← 扫描异常Token │
│ │ (定时任务) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ send_alert │ ← 企微机器人告警 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 2.2 数据结构
#### Redis Key设计
| Key格式 | 类型 | TTL | 说明 |
|---------|------|-----|------|
| `token_ips:{token_hash}` | Set | 3600秒 | 记录Token使用的IP集合 |
| `token_ips:alerted:{token_hash}` | String | 3600秒 | 已告警标记,避免重复 |
#### Token存储(现有)
| Key格式 | 类型 | 说明 |
|---------|------|------|
| `user:token:{token}` | JSON | 用户信息,含employee_id |
### 2.3 接口设计
#### 2.3.1 记录Token使用IP (埋点)
```python
# 在 token_service.py 中增加
async def record_token_ip(token: str, ip: str):
"""
记录Token使用的IP地址
Args:
token: 用户Token
ip: 客户端IP (X-Forwarded-For 或 request.client.host)
"""
import hashlib
token_hash = hashlib.sha256(token.encode()).hexdigest()
redis = await get_redis()
key = f"token_ips:{token_hash}"
# 添加IP到Set (自动去重)
redis.sadd(key, ip)
# 设置1小时过期
redis.expire(key, 3600)
```
#### 2.3.2 异常检测定时任务
```python
# 在 tasks/token_anomaly_detection.py
async def detect_token_anomaly():
"""
检测Token异常使用
扫描所有 token_ips:* keys
当 IP数量 >= 阈值 时触发告警
"""
# 配置
THRESHOLD = 3 # IP数量阈值
WINDOW_SECONDS = 3600 # 时间窗口
redis = await get_redis()
alerted_key_prefix = "token_ips:alerted:"
# 扫描所有 token_ips:* keys
async for key in redis.scan_iter("token_ips:*"):
# 跳过 alerted keys
if key.startswith(alerted_key_prefix):
continue
token_hash = key.replace("token_ips:", "")
ip_count = await redis.scard(key)
if ip_count >= THRESHOLD:
# 检查是否已告警
alerted_key = f"{alerted_key_prefix}{token_hash}"
if await redis.get(alerted_key):
continue # 已告警,跳过
# 获取用户信息
token = await redis.get(f"user:token:{token_hash}")
if token:
user_data = json.loads(token)
employee_id = user_data.get("employee_id")
# 发送告警
await send_security_alert(
title="Token异常告警",
content=f"员工 {employee_id} 的Token被 {ip_count} 个IP使用\nToken: {token_hash[:8]}..."
)
# 标记已告警
await redis.setex(alerted_key, WINDOW_SECONDS, "1")
```
#### 2.3.3 获取客户端IP
```python
def get_client_ip(request) -> str:
"""获取客户端真实IP"""
# 优先从 X-Forwarded-For 获取
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
# 降级到 request.client.host
return request.client.host if request.client else ""
```
---
## 3. 配置项
### 3.1 环境变量
| 变量名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `TOKEN_ANOMALY_THRESHOLD` | int | 3 | 触发告警的IP数量阈值 |
| `TOKEN_ANOMALY_WINDOW` | int | 3600 | 时间窗口(秒) |
| `TOKEN_ANOMALY_AUTO_DISABLE` | bool | false | 是否自动禁用Token |
| `CONTENT_AUDIT_WEBHOOK` | string | - | 企微机器人webhook(现有) |
---
## 4. 告警内容
### 4.1 告警模板
```json
{
"msgtype": "markdown",
"markdown": {
"content": "🔴 **Token异常告警**\n\n"
"> 员工ID: {employee_id}\n"
"> 异常Token: {token_hash[:8]}...\n"
"> IP数量: {ip_count}\n"
"> 时间: {timestamp}\n\n"
"> **请及时确认是否为本人操作**"
}
}
```
---
## 5. 集成点
### 5.1 现有组件复用
| 组件 | 用途 |
|------|------|
| Redis | IP存储 |
| APScheduler | 定时任务 |
| content_audit_webhook | 企微告警 |
| token_service.py | Token管理 |
### 5.2 侵入点
| 文件 | 修改内容 |
|------|----------|
| `app/services/token_service.py` | 增加 record_token_ip() |
| `app/main.py` | 注册定时任务 |
| `app/tasks/token_anomaly_detection.py` | 新建检测任务 |
---
## 6. 性能与容错
### 6.1 性能估算
| 指标 | 估算值 |
|------|---------|
| Redis存储 | ~50KB (1000活跃Token) |
| 定时任务耗时 | < 100ms |
| 定时任务间隔 | 60秒 |
### 6.2 容错设计
- 告警发送失败:记录日志,不阻塞主流程
- Redis连接失败:跳过本次检测,下个周期重试
- Token不存在:跳过,不影响其他检测
---
## 7. 测试用例
### 7.1 单元测试
| 用例ID | 描述 | 预期结果 |
|--------|------|----------|
| T001 | 单IP使用Token | 不触发告警 |
| T002 | 3个IP使用Token | 触发告警 |
| T003 | 5个IP使用Token | 触发告警(严重) |
| T004 | 同一IP多次使用 | 不触发告警 |
### 7.2 集成测试
| 用例ID | 描述 | 预期结果 |
|--------|------|----------|
| I001 | 真实Token请求 | IP被记录 |
| I002 | 定时任务执行 | 异常Token被检测 |
| I003 | 告警发送 | 企微收到消息 |
---
## 8. 部署清单
### 8.1 文件变更
| 操作 | 文件 |
|------|------|
| 新增 | `app/tasks/token_anomaly_detection.py` |
| 修改 | `app/services/token_service.py` |
| 修改 | `app/main.py` |
### 8.2 配置变更
| 操作 | 变量 |
|------|------|
| 新增(可选) | `TOKEN_ANOMALY_THRESHOLD` |
| 新增(可选) | `TOKEN_ANOMALY_AUTO_DISABLE` |
---
## 9. 回滚方案
如需回滚:
1. 移除定时任务注册 (main.py)
2. 删除 record_token_ip() 调用
3. Redis keys 会在1小时后自动过期
---
> **编制人**: 威胁检测工程师
> **日期**: 2026-07-14
> **审核人**: 待定