Files
wecom_it_smart_desk/docs/04-运维文档/部署运维/技术设计-Token多IP异常检测.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

292 lines
8.7 KiB
Markdown

# 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
> **审核人**: 待定