292 lines
8.7 KiB
Markdown
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
|
||
|
|
> **审核人**: 待定
|