# ============================================================================= # 企微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())