v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化
This commit is contained in:
+184
-5
@@ -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-01(nginx 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:再次发生,返回 500(rewrite 循环 `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。
|
||||
- **误判历程**:
|
||||
|
||||
@@ -164,6 +164,40 @@ ping itsupport.servyou.com.cn
|
||||
# CORS_ORIGINS=http://10.90.5.110
|
||||
```
|
||||
|
||||
### 4.3 部署铁律(2026-07-17 新增,踩坑记录)
|
||||
|
||||
以下 3 条均为生产事故复盘得出的硬性规则,部署/变更时必须遵守:
|
||||
|
||||
#### 铁律 1:backend 必须 `--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` 未声明导致扫码登录崩溃)
|
||||
|
||||
#### 铁律 3:Redis 密码含特殊字符必须 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
|
||||
> **审核人**: 待定
|
||||
Reference in New Issue
Block a user