Files
wecom_it_smart_desk/backend/app/tasks/token_anomaly_detection.py
T

194 lines
6.0 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — Token异常检测定时任务
# =============================================================================
# 说明:定时检测Token异常使用行为:
# 1. 同一Token在多个不同IP使用(可能凭证泄露)
# 2. 检测时间窗口:1小时内
# 3. 告警阈值:>=3个不同IP
# 运行频率:每5分钟执行一次
# =============================================================================
import logging
from datetime import datetime
import redis.asyncio as aioredis
from app.config import settings
from app.services.token_service import TokenService
logger = logging.getLogger(__name__)
# 异常检测配置
TOKEN_ANOMALY_IP_THRESHOLD = 3 # 同一Token使用不同IP数量阈值
TOKEN_ANOMALY_TIME_WINDOW = 3600 # 检测时间窗口(秒)
# Redis Key前缀
TOKEN_IP_PREFIX = "token_ip:"
async def detect_token_ip_anomaly():
"""检测Token异常使用行为。
检测逻辑:
1. 扫描所有 token_ip:* 的Key
2. 解析IP列表,统计不同IP数量
3. 超过阈值则触发告警
"""
try:
# 创建Redis客户端
redis_client = settings.create_redis_client()
token_service = TokenService(redis_client)
# 扫描所有Token IP记录
cursor = 0
anomaly_count = 0
anomalies = []
while True:
cursor, keys = await redis_client.scan(
cursor=cursor,
match=f"{TOKEN_IP_PREFIX}*",
count=100
)
for key in keys:
# 解析key获取token_hash
key_str = key.decode("utf-8") if isinstance(key, bytes) else key
token_hash = key_str.replace(f"{TOKEN_IP_PREFIX}", "")
# 获取IP记录
data = await redis_client.get(key)
if not data:
continue
data_str = data.decode("utf-8") if isinstance(data, bytes) else data
# 解析IP列表
ips = set()
for entry in data_str.split(","):
if "@" in entry:
ip, _ = entry.rsplit("@", 1)
ips.add(ip)
# 检测异常
if len(ips) >= TOKEN_ANOMALY_IP_THRESHOLD:
anomaly_count += 1
anomalies.append({
"token_hash": token_hash,
"ip_count": len(ips),
"ips": list(ips),
"timestamp": datetime.now().isoformat()
})
logger.warning(
f"检测到Token异常使用: token_hash={token_hash}, "
f"ip_count={len(ips)}, ips={list(ips)}"
)
if cursor == 0:
break
# 发送告警
if anomalies:
await _send_anomaly_alert(anomalies)
logger.info(f"Token异常检测完成: 检测到 {anomaly_count} 个异常")
await redis_client.aclose()
except Exception as e:
logger.error(f"Token异常检测任务执行失败: {e}", exc_info=True)
async def _send_anomaly_alert(anomalies: list):
"""发送Token异常告警。
通过企微机器人发送告警消息。
Args:
anomalies: 异常列表
"""
if not anomalies:
return
# 检查是否配置了webhook
webhook = getattr(settings, "content_audit_webhook", None)
if not webhook:
logger.warning("未配置 content_audit_webhook,跳过告警")
return
try:
import httpx
# 构建告警消息
lines = ["🚨 **Token异常使用告警**\n"]
lines.append(f"**检测时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"**异常数量**: {len(anomalies)}")
lines.append("\n**异常详情:**")
for i, a in enumerate(anomalies[:5], 1): # 最多显示5条
ips_str = ", ".join(a["ips"][:3])
if len(a["ips"]) > 3:
ips_str += f" ... (+{len(a['ips']) - 3} more)"
lines.append(f"{i}. Token: `{a['token_hash'][:8]}...`")
lines.append(f" IP数量: {a['ip_count']}, IPs: {ips_str}")
if len(anomalies) > 5:
lines.append(f"\n... 还有 {len(anomalies) - 5} 条异常")
lines.append("\n⚠️ **建议**: 立即检查是否为凭证泄露,必要时禁用相关账户")
content = "\n".join(lines)
# 发送企微机器人消息
async with httpx.AsyncClient(timeout=10) as client:
await client.post(
webhook,
json={
"msgtype": "markdown",
"markdown": {
"content": content
}
}
)
logger.info(f"Token异常告警已发送: {len(anomalies)}")
except Exception as e:
logger.error(f"发送Token异常告警失败: {e}")
async def test_token_anomaly_detection():
"""测试用:模拟Token多IP使用场景。
创建测试数据,验证检测逻辑。
"""
import hashlib
redis_client = settings.create_redis_client()
token_service = TokenService(redis_client)
# 生成测试token
test_token = "test_token_anomaly_12345"
token_hash = hashlib.sha256(test_token.encode()).hexdigest()[:16]
# 模拟同一token使用3个不同IP
key = f"{TOKEN_IP_PREFIX}{token_hash}"
test_data = (
"192.168.1.100@2026-07-14T10:00:00,"
"192.168.1.101@2026-07-14T10:05:00,"
"192.168.1.102@2026-07-14T10:10:00"
)
await redis_client.setex(key, 3600, test_data)
logger.info(f"测试数据已创建: key={key}, data={test_data}")
await redis_client.aclose()
logger.info("测试数据创建完成,请运行检测任务验证")
if __name__ == "__main__":
import asyncio
# 直接运行时执行测试
asyncio.run(test_token_anomaly_detection())