2026-03-04 00:18:33 +08:00
# 产品需求文档 (PRD)
# Key-IP Sentinel: 大模型 API 密钥 IP 防泄漏网关
**版本**: v1.1 | **状态 ** :待开发 | **作者 ** :算力平台管理员
***
## 一、产品背景与目标
### 1.1 背景
企业内部算力平台(基于 New API + vLLM 自部署)面向几百至上千名内部研发人员开放使用。用户注册账号后可自助申请 API Key, 但现有机制无法防止用户将 API Key 分享给外部人员或在未授权设备上使用,存在算力被盗刷的风险。
### 1.2 解决方案定位
在调用方与 New API 之间部署一个独立的轻量级反向代理服务 **Key-IP Sentinel ** ,实现"**首次使用即绑定( First-Use-Bind) **"机制——API Key 在第一次被调用时,系统自动将该 Key 与发起调用的客户端 IP 绑定;此后该 Key 只能从绑定的 IP 发起请求,其他 IP 全部拦截。整个过程对用户无感知,无需管理员手动介入。
### 1.3 产品目标
- 彻底杜绝 API Key 被分享到外部或其他未授权设备上使用。
- 系统自动完成 IP 绑定,管理员只需处理少数"换 IP"的运维操作。
- 提供清晰的管理后台,支持查看绑定状态、拦截日志、手动运维。
***
## 二、整体架构
### 2.1 流量链路
```
2026-03-04 13:59:38 +08:00
调用方 (Client)
│
│ HTTP (80)
▼
┌─────────────────────────────────────────┐
│ Nginx │
│ 职责:路径路由 / │
│ 静态文件 / 内网鉴权 / 粗粒度限流 │
└────────────────┬────────────────────────┘
│ HTTP 内网转发
2026-03-04 00:18:33 +08:00
▼
┌─────────────────────────────────────────┐
│ Key-IP Sentinel App │
│ 职责: Token提取 / IP绑定校验 / │
│ 代理转发 / 管理 API / 告警 │
└───────┬─────────────────┬───────────────┘
│ │ 异步写
┌────▼────┐ ┌─────▼──────┐
│ Redis │ │ PostgreSQL │
│ (热缓存) │ │ (持久化) │
└─────────┘ └────────────┘
│
▼
┌─────────────────────────────────────────┐
│ New API (UDPI) │
│ 职责:用户鉴权 / 额度管理 / 计费 / 路由 │
└───────────────┬─────────────────────────┘
│
┌──────▼──────┐
│ vLLM 节点 │
└─────────────┘
```
### 2.2 技术选型
| 模块 | 技术方案 | 说明 |
|---|---|---|
| 反向代理 & 业务后端 | **Go (Gin) ** 或 **Python (FastAPI) ** | 优先推荐 Go, 高并发下内存占用极低, SSE 透传天然支持 |
| 缓存层 | **Redis 7+ ** | Token-IP 绑定热数据, TTL 7 天 |
| 持久化层 | **PostgreSQL 15+ ** | 绑定记录与审计日志,使用 `inet` /`cidr` 原生类型做 IP 范围匹配 |
| 前端管理 UI | **Vue3 + Element Plus ** | 纯静态 SPA, 打包后由 Nginx 直接托管 |
2026-03-04 13:59:38 +08:00
| 外层网关 | **Nginx ** | 路径隔离、静态文件、`limit_req_zone` 限流 |
2026-03-04 00:18:33 +08:00
| 部署方式 | **Docker Compose ** | 共 4 个容器: nginx / sentinel-app / redis / postgres |
***
## 三、核心业务流程
### 3.1 请求拦截与动态绑定(网关核心逻辑)
```
收到请求
│
├─ 无 Authorization Header? ─→ 直接透传给 New API( 由其拒绝)
│
├─ 提取 Token( sk-xxx)
├─ 提取真实 IP( 只信任 Nginx 写入的 X-Real-IP, 见安全说明 §6.1)
├─ 对 Token 做 HMAC-SHA256 哈希 → token_hash
│
├─ 查 Redis: sentinel:token:{token_hash}
│ │
│ 命中 ──→ 比对 IP( 支持 CIDR 匹配)
│ │ ├─ 匹配 → 刷新 last_used_at( 异步写 PG) → 放行
│ │ └─ 不匹配 → 写拦截日志 → 检查告警阈值 → 返回 403
│ │
│ 未命中 → 查 PG: SELECT * FROM token_bindings WHERE token_hash=?
│ │
│ 有记录 → 回写 Redis → 比对 IP( 同上)
│ │
│ 无记录 → 【首次绑定】
│ ├─ INSERT INTO token_bindings (token_hash, bound_ip, status=Active)
│ ├─ 写入 Redis, TTL 7天
│ └─ 放行请求
```
### 3.2 SSE 流式输出透传要求
- 网关必须支持 **逐 chunk 实时转发 ** ,不得缓冲整个响应再转发。
- 响应 Header 中的 `Content-Type: text/event-stream` 和 `Transfer-Encoding: chunked` 必须透传。
- 连接超时时间应设为不低于 **600 秒 ** (大模型长上下文推理可能耗时较长)。
### 3.3 管理员运维场景
| 场景 | 触发条件 | 管理员操作 | 系统行为 |
|---|---|---|---|
| 用户换电脑/换 IP | 用户反馈 Key 无法使用 | 在后台搜索该 Token, 点击【解绑】 | 清除 PG 记录 + 清除 Redis 缓存,用户下次调用重新触发首次绑定 |
| 用户转岗,需绑定新 IP | 用户提交申请 | 在后台点击【编辑 IP】, 填入新 IP 或网段 | 更新 PG + 使 Redis 缓存失效(强制下次从 PG 重新加载)|
| 发现 Key 被泄露 | 拦截告警推送 | 点击【封禁】 | 将该 Token 状态置为 Banned, Redis 同步更新,后续所有请求直接被 Sentinel 拒绝 |
| 用户离职 | HR 通知 | 点击【封禁】 | 同上 |
***
## 四、功能需求
### 4.1 Nginx 层职责
在 `nginx.conf` 中需要实现以下配置:
```nginx
2026-03-04 13:59:38 +08:00
# 1. 代理路径:/ 全部转发给 sentinel-app:7000
# 2. 管理后台访问限制
location /admin/ {
allow 10.0.0.0/8; # 内网 IP 段
allow 192.168.0.0/16;
deny all;
proxy_pass http://sentinel-app:7000;
}
# 3. 静态文件(前端 UI)
location /admin/ui/ {
root /etc/nginx/html;
try_files $uri $uri/ /admin/ui/index.html;
}
# 4. 基础限流
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
# 5. 强制写入真实 IP( 防客户端伪造)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr; # 覆盖,不信任客户端传入的值
```
2026-03-04 00:18:33 +08:00
### 4.2 Sentinel App 反向代理模块
- **受信 IP Header**:只读取 `X-Real-IP` ( Nginx 写入的),忽略请求中原始的 `X-Forwarded-For` 。
- **Token 提取**:从 `Authorization: Bearer {token}` Header 中提取。
- **连接池**:与下游 New API 保持 HTTP Keep-Alive 长连接,连接池最大连接数可通过配置项设置,默认 512。
- **异步落库**:放行后,`last_used_at` 更新通过内部消息通道异步批量写入 PG( 每 5 秒 flush 一次),避免阻塞代理主协程。
- **降级策略**:
- Redis 不可用时:降级查询 PG, 同时触发内部限流( 最大 QPS 降为正常值的 50%),并写入系统告警日志。
- PG 也不可用时:行为由环境变量 `SENTINEL_FAILSAFE_MODE` 决定:`open` (放行所有,保业务)或 `closed` (拒绝所有,保安全),生产环境默认 `closed` 。
### 4.3 Web 管理后台( Admin UI)
**页面一: 数据大盘( Dashboard) **
- 卡片指标:
- 今日总请求数
- 今日放行数 / 拦截数
- 当前已绑定 Token 数量
- 当前封禁 Token 数量
- 折线图:最近 7 天每日放行 vs 拦截数量趋势
- 列表:最近 10 条拦截记录(实时刷新)
**页面二: 绑定管理( Bindings) **
表格字段:
| 字段 | 说明 |
|---|---|
| ID | 数据库主键 |
| Token( 脱敏) | 展示格式 `sk-ab**...**xy` (前 4 后 4) |
| 绑定 IP / 网段 | 支持展示 CIDR, 如 `192.168.1.0/24` |
| 状态 | `Active` / `Banned` |
| 首次绑定时间 | |
| 最近调用时间 | |
| 操作 | 解绑 / 编辑 IP / 封禁 / 解封 |
筛选功能:按 Token 尾号搜索、按 IP 搜索、按状态筛选。
**页面三: 拦截日志( Intercept Logs) **
表格字段:
| 字段 | 说明 |
|---|---|
| 时间 | 拦截发生的精确时间 |
| Token( 脱敏) | 被拦截的 Token |
| 绑定 IP | 该 Token 注册的合法 IP |
| 尝试 IP | 发起非法请求的 IP |
| 是否已告警 | 是否触发过告警推送 |
支持按时间范围、Token、尝试 IP 筛选,支持导出 CSV。
**页面四: 系统设置( Settings) **
- 告警阈值配置: N 分钟内同一 Token 被拦截 M 次触发告警( 默认: 5 分钟内 5 次)。
- 告警方式: Webhook URL( 调用方自定义, POST JSON 格式)。
- 自动归档策略: last_used_at 超过 N 天的记录自动归档(默认 90 天)。
- `FAILSAFE_MODE` 开关显示与切换。
### 4.4 Admin REST API
所有 `/admin/api/*` 接口需要携带 JWT Token( 通过 `/admin/api/login` 获取),登录凭证从环境变量读取。
| Method | 路径 | 说明 |
|---|---|---|
| `POST` | `/admin/api/login` | 管理员登录,返回 JWT |
| `GET` | `/admin/api/dashboard` | 大盘统计数据 |
| `GET` | `/admin/api/bindings` | 获取绑定列表(支持分页 & 筛选) |
| `POST` | `/admin/api/bindings/unbind` | 解除绑定(清除 PG + Redis) |
| `PUT` | `/admin/api/bindings/ip` | 手动修改绑定 IP |
| `POST` | `/admin/api/bindings/ban` | 封禁 Token |
| `POST` | `/admin/api/bindings/unban` | 解封 Token |
| `GET` | `/admin/api/logs` | 获取拦截日志(分页 & 筛选) |
| `GET` | `/admin/api/logs/export` | 导出日志 CSV |
| `GET` | `/admin/api/settings` | 获取当前系统配置 |
| `PUT` | `/admin/api/settings` | 更新系统配置 |
| `GET` | `/health` | 健康检查(无需鉴权,供 Nginx/Docker 使用)|
***
## 五、数据结构设计
### 5.1 `token_bindings`(主表)
```sql
CREATE TABLE token_bindings (
id BIGSERIAL PRIMARY KEY,
token_hash VARCHAR(64) NOT NULL UNIQUE, -- HMAC-SHA256 哈希值
token_display VARCHAR(20) NOT NULL, -- 脱敏展示用,如 sk-ab****xy
bound_ip CIDR NOT NULL, -- 使用 PG 原生 CIDR 类型,支持网段
status SMALLINT NOT NULL DEFAULT 1, -- 1=Active, 2=Banned
first_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_token_bindings_token_hash ON token_bindings(token_hash);
CREATE INDEX idx_token_bindings_bound_ip ON token_bindings USING GIST (bound_ip inet_ops);
```
IP 范围匹配查询(使用 PG `<<` 操作符,性能极高):
```sql
-- 检查请求 IP 是否在绑定的网段内
SELECT status FROM token_bindings
WHERE token_hash = $1
AND $2::inet << bound_ip -- $2 为请求方真实 IP
LIMIT 1;
```
### 5.2 `intercept_logs`(拦截审计日志表)
```sql
CREATE TABLE intercept_logs (
id BIGSERIAL PRIMARY KEY,
token_hash VARCHAR(64) NOT NULL,
token_display VARCHAR(20) NOT NULL,
bound_ip CIDR NOT NULL,
attempt_ip INET NOT NULL,
alerted BOOLEAN NOT NULL DEFAULT FALSE,
intercepted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_intercept_logs_token_hash ON intercept_logs(token_hash);
CREATE INDEX idx_intercept_logs_intercepted_at ON intercept_logs(intercepted_at DESC);
```
### 5.3 Redis 数据结构
**绑定记录缓存**
- **Key**: `sentinel:binding:{token_hash}`
- **Value**( JSON) :
```json
{
"bound_ip": "192.168.1.0/24",
"status": 1
}
```
- **TTL**: 604800 秒( 7 天),每次命中时刷新
**拦截计数器(用于告警)**
- **Key**: `sentinel:alert:{token_hash}`
- **Value**: Integer( 拦截次数计数)
- **TTL**:由告警配置的时间窗口决定(默认 300 秒/5 分钟)
- 当计数达到阈值时, App 触发告警并重置计数器
***
## 六、安全规范
### 6.1 IP 防伪造(最高优先级)
- Nginx 必须使用 `proxy_set_header X-Real-IP $remote_addr;` 强制覆盖,`$remote_addr` 是 TCP 层观察到的连接 IP, 客户端**无法伪造**。
- Sentinel App 内部必须明确配置受信上游 IP 列表(即 Nginx 容器的内网 IP, 如 `172.18.0.2` ),只有来自受信上游的 `X-Real-IP` 才被采信,否则直接用 TCP 连接的原始 IP。
- **禁止**在任何情况下直接信任客户端传入的 `X-Forwarded-For` Header。
### 6.2 Token 存储安全
- **禁止明文存储** API Key 的完整内容。
- 存储时使用 **HMAC-SHA256 ** (密钥从环境变量 `SENTINEL_HMAC_SECRET` 读取, 32 字节随机字符串)对 Token 进行哈希。
- `token_display` 字段仅存储供人识别的脱敏格式(`sk-ab****xy` ),无法被逆推出原始 Key。
### 6.3 Admin 接口安全
- 管理员登录接口需要防暴力破解:连续 5 次失败则锁定 IP 15 分钟。
- JWT Token 有效期设为 8 小时,支持手动吊销(退出登录清除服务端 Session 记录)。
- 所有管理接口的调用需要记录操作日志(操作人 IP + 操作内容)。
***
## 七、环境变量配置清单
AI 开发时应将所有配置项做成环境变量,以下为完整列表:
| 环境变量名 | 必填 | 说明 | 示例值 |
|---|---|---|---|
| `DOWNSTREAM_URL` | ✅ | 下游 New API 的地址 | `http://new-api:3000` |
| `REDIS_ADDR` | ✅ | Redis 连接地址 | `redis:6379` |
| `REDIS_PASSWORD` | | Redis 密码 | |
| `PG_DSN` | ✅ | PostgreSQL 连接串 | `postgres://user:pass@postgres:5432/sentinel` |
| `SENTINEL_HMAC_SECRET` | ✅ | Token 哈希的 HMAC 密钥( 32字节) | 随机生成 |
| `ADMIN_PASSWORD` | ✅ | 管理员登录密码 | |
| `ADMIN_JWT_SECRET` | ✅ | JWT 签名密钥 | 随机生成 |
| `TRUSTED_PROXY_IPS` | ✅ | 受信上游代理 IP( Nginx 的内网 IP) | `172.18.0.2` |
| `SENTINEL_FAILSAFE_MODE` | ✅ | 全链路故障时行为:`open` /`closed` | `closed` |
| `APP_PORT` | | App 监听端口 | `7000` |
| `ALERT_WEBHOOK_URL` | | 告警推送 Webhook 地址 | |
| `ALERT_THRESHOLD_COUNT` | | 触发告警的拦截次数阈值 | `5` |
| `ALERT_THRESHOLD_SECONDS` | | 告警计数时间窗口(秒)| `300` |
| `ARCHIVE_DAYS` | | 自动归档的不活跃天数 | `90` |
***
## 八、Docker Compose 部署结构
```yaml
version: '3.8'
services:
nginx:
2026-03-04 13:59:38 +08:00
image: nginx:alpine
container_name: sentinel-nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./frontend/dist:/etc/nginx/html/admin/ui:ro
2026-03-04 00:18:33 +08:00
depends_on:
- sentinel-app
networks:
- sentinel-net
sentinel-app:
image: key-ip-sentinel:latest # 本地 build
container_name: sentinel-app
build: .
restart: always
# 不暴露端口到宿主机,只在内网被 Nginx 访问
environment:
- DOWNSTREAM_URL=http://new-api:3000 # 通过 external network 访问 New API
- REDIS_ADDR=redis:6379
- PG_DSN=postgres://sentinel:password@postgres:5432/sentinel
- SENTINEL_HMAC_SECRET=${SENTINEL_HMAC_SECRET}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
- TRUSTED_PROXY_IPS=172.18.0.0/16
- SENTINEL_FAILSAFE_MODE=closed
depends_on:
- redis
- postgres
networks:
- sentinel-net
- llm-shared-net # 与 New API 的共享网络( external)
redis:
image: redis:7-alpine
container_name: sentinel-redis
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- sentinel-net
postgres:
image: postgres:15
container_name: sentinel-postgres
restart: always
environment:
POSTGRES_USER: sentinel
POSTGRES_PASSWORD: ${PG_PASSWORD}
POSTGRES_DB: sentinel
volumes:
- pg_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # 初始化建表 SQL
networks:
- sentinel-net
volumes:
redis_data:
pg_data:
networks:
sentinel-net:
driver: bridge
llm-shared-net:
external: true # 与 New API 共享的已存在网络
```
***
## 九、非功能需求
| 指标 | 要求 |
|---|---|
| **代理延迟增加( p99) ** | Redis 命中路径增加延迟 ≤ 5ms |
| **并发能力 ** | 单实例支持 ≥ 1000 并发连接( Go 实现)|
| **SSE 透传 ** | 流式输出无缓冲实时转发,延迟增加 ≤ 10ms |
| **可扩展性 ** | App 本体无状态,可横向扩展多实例,负载由 Nginx upstream 均衡 |
| **日志格式 ** | 结构化 JSON 日志,兼容 ELK / Loki 采集 |
| **健康检查 ** | `GET /health` 需 200ms 内响应 `{"status":"ok"}` |
***