- 搭建 FastAPI、Redis、PostgreSQL、Nginx 与 Docker Compose 基础结构 - 实现反向代理、首用绑定、拦截告警、归档任务和管理接口 - 提供 Vue3 管理后台初版,以及 uv/requirements 双依赖配置
425 lines
17 KiB
Markdown
425 lines
17 KiB
Markdown
# 产品需求文档 (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 流量链路
|
||
|
||
```
|
||
调用方 (Client)
|
||
│
|
||
│ HTTPS (443)
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ Nginx │
|
||
│ 职责:TLS终止 / 路径路由 / │
|
||
│ 静态文件 / 内网鉴权 / 粗粒度限流 │
|
||
└────────────────┬────────────────────────┘
|
||
│ HTTP 内网转发
|
||
▼
|
||
┌─────────────────────────────────────────┐
|
||
│ 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 直接托管 |
|
||
| 外层网关 | **Nginx** | TLS 终止、路径隔离、静态文件、`limit_req_zone` 限流 |
|
||
| 部署方式 | **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
|
||
# 1. TLS 终止(HTTPS → HTTP 转发给 sentinel-app)
|
||
# 2. 代理路径:/ 全部转发给 sentinel-app:7000
|
||
# 3. 管理后台访问限制
|
||
location /admin/ {
|
||
allow 10.0.0.0/8; # 内网 IP 段
|
||
allow 192.168.0.0/16;
|
||
deny all;
|
||
proxy_pass http://sentinel-app:7000;
|
||
}
|
||
# 4. 静态文件(前端 UI)
|
||
location /admin/ui/ {
|
||
root /etc/nginx/html;
|
||
try_files $uri $uri/ /admin/ui/index.html;
|
||
}
|
||
# 5. 基础限流
|
||
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
|
||
# 6. 强制写入真实 IP(防客户端伪造)
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $remote_addr; # 覆盖,不信任客户端传入的值
|
||
```
|
||
|
||
### 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:
|
||
image: nginx:alpine
|
||
container_name: sentinel-nginx
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
volumes:
|
||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||
- ./frontend/dist:/etc/nginx/html/admin/ui:ro
|
||
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"}` |
|
||
|
||
***
|