pracda/llm-api-gateway

GitHub: pracda/llm-api-gateway

基于 Java/Spring Boot 的生产级多租户 LLM API 网关,代理 OpenAI 与 Anthropic 调用,提供身份认证、限流、prompt 注入检测、PII 防御、成本控制和审计日志等集中式安全管控。

Stars: 0 | Forks: 0

# 安全 LLM API Gateway [![CI / CD](https://static.pigsec.cn/wp-content/uploads/repos/cas/a9/a94aa46681ea74ca7522a15bc35b464c7bc7c00b2e8cbbb7650736c821a1eed4.svg)](https://github.com/pracda/llm-api-gateway/actions/workflows/ci.yml) ![Java](https://img.shields.io/badge/Java-17-orange) ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2-brightgreen) 一个多租户 API Gateway,部署在 **OpenAI** 和 **Anthropic** 前方,为每一个通过它调用 LLM 的团队提供集中的安全性、滥用检测和用量可见性 —— 任何团队都无需直接接触提供商密钥。 客户端应用无需直接调用 LLM API,而是使用签发的 API key 调用此网关。它负责处理身份验证、限流、prompt 注入和 PII 防御、带有自动锁定功能的威胁检测、按请求的意图分类,以及完整的审计日志 —— 然后返回模型的响应(或者附上原因拦截请求)。 ## 为什么需要它 那些将 LLM 调用直接硬编码到应用中的团队,最终会导致提供商密钥散落在各个服务中,没有共享的限流机制,无法可见谁发送了什么类型的流量,并且在出现滥用情况时无法做出反应。此网关将所有这些集中在一个地方:为每个团队签发具有作用域的 API key,实时监控每个请求,并在出现异常的那一刻(在演变为事件之前)获取证据链。 ## 功能 **安全管道(每个请求)** - 针对人类(账户/组织管理)的 JWT 认证和针对机器流量的 API key 认证 —— 分开执行,仅凭有效的 JWT 不足以调用 `/chat` - 带有 0-100 越狱风险评分的 prompt 注入检测(OWASP LLM01) - 输入/输出 PII 检测与脱敏 —— 电子邮件、电话号码、社会安全号码 (SSN)、银行卡号、内部 IP - 针对 XSS、SQL/shell 注入回显和凭证泄露的输出净化(OWASP LLM05) - 带有自动 API key 撤销的金丝雀 token system prompt 泄露检测 - 基于用户/天/全局的限流(基于 Redis 的滑动窗口),外加 **`/auth/register` 和 `/auth/token` 上的基于 IP 的限流**,以在任何流量到达账户锁定逻辑之前阻止注册垃圾邮件和分布式暴力破解登录尝试 - 针对疑似模型提取滥用的持续成功量检测(OWASP LLM10) - 每个提供商的模型名称允许列表 —— 无法识别的 `model` 值会快速失败并返回 `400`,而不会被转发到提供商 API 并返回令人困惑的 `502` **成本控制与可靠性** - 每个请求的 $ 成本都是根据配置的按模型定价 (`app.llm.pricing`) 从实际 token 使用量计算得出的,并记录在其审计日志中 - 每个 API key 的可选每日 $ 预算 (`dailyBudgetUsd`,基于等级的默认值) —— 一旦运行中的每日支出(在 Redis 中跟踪)超过上限,将强制执行 `402 Payment Required` - 提供商瞬时故障(5xx/超时)时自动重试,如果主要提供商持续失败,则带有**跨提供商回退**(OpenAI ↔ Anthropic)—— 响应始终通过 `requestedProvider`/`fellBack` 字段明确指示发生了回退,而不是静默地返回不同模型的输出。流式传输仅在发送第一个数据块之前进行重试;它从不在流传输中途回退,因为已经交付的 token 无法撤回 **多租户与访问控制** - 具有 Trial / Standard / Enterprise API key 级别的组织(限流、token 上限、过期时间) - 自助式成员配置 —— 将用户添加到组织会自动创建其账户,无需单独的注册步骤 - 区分于自动临时锁定的按用户手动封锁(禁用账户 + 撤销所有 key) **威胁检测与证据** - 在重复的注入尝试、输出拦截、限流滥用或登录暴力破解时,自动、自我重新武装的账户锁定 - 跨账户协同攻击关联(来自多个账户的相同攻击签名) - 对每个请求进行基于规则的意图分类 (`NORMAL` / `SUSPICIOUS` / `MALICIOUS`),完全源自管道中已计算的信号 —— **原始的 prompt 和响应永远不会被存储**,仅存储 SHA-256 哈希(OWASP LLM06) - 按用户的证据追踪:意图细分、token/数量趋势、行为偏差标记、警报历史记录 —— 这是管理员在封锁账户前所需的一切 - 可选的高严重性警报 Slack/webhook 推送 **可见性** - 实时操作仪表盘(Server-Sent Events)—— 实时请求流、风险排名、攻击时间线、IP 黑名单管理 - 按组织的趋势视图和活动日志 - 每个组织可下载的每周/每月 PDF 活动报告(按成员的请求/token/可疑计数) **流式传输** - 逐 token 的流式传输 (`/chat/stream`),具有相同的安全管道 —— 输出扫描在流式响应交付后运行,因为内容无法撤回,但它仍然会发出警报并且可以自动撤销 key ## 请求管道 ``` Client │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ Auth — JWT (human) or X-API-Key (machine) │ ├────────────────────────────────────────────────────────────────┤ │ IP blocklist + account lockout check │ ├────────────────────────────────────────────────────────────────┤ │ Rate limit — per-user / per-day / global (Redis) │ ├────────────────────────────────────────────────────────────────┤ │ Budget check — reject if this key's daily $ spend is exceeded │ ├────────────────────────────────────────────────────────────────┤ │ Input scan — injection detection, jailbreak score, PII │ ├────────────────────────────────────────────────────────────────┤ │ LLM call — OpenAI or Anthropic (model allow-list validated, │ │ retried on transient failure, falls back to the other provider) │ ├────────────────────────────────────────────────────────────────┤ │ Output scan — canary leak, PII redaction, unsafe content │ ├────────────────────────────────────────────────────────────────┤ │ Intent classification — NORMAL / SUSPICIOUS / MALICIOUS │ ├────────────────────────────────────────────────────────────────┤ │ Audit log (async) — SHA-256 prompt hash only, never raw content │ └────────────────────────────────────────────────────────────────┘ │ ▼ Response to client + live event pushed to the admin dashboard ``` 被拦截的请求会在捕获它的任何阶段发生短路(429 表示限流,402 表示超出每日预算,400 表示注入/PII/不安全输出/无效模型,423 表示锁定,403 表示被封锁的 IP)—— 它仍然会被记录下来并附上原因,并为证据链进行分类。 预算检查只能看到*本次调用之前*的运行中支出 —— 在 LLM 响应之前,无法确切知道处理中请求的精确成本 —— 因此它捕获的是“已经超预算”,而不是“这个特定的调用会让你超支”。实际成本是在 LLM 响应后立即计算并记录的,这正是*下一个*请求会被拦截的原因。 ## 技术栈 | 层级 | 技术 | |---|---| | 后端 | Java 17, Spring Boot 3.2 | | 安全 | Spring Security, JJWT | | LLM 提供商 | 通过 Spring WebFlux `WebClient` 调用 OpenAI 和 Anthropic REST API | | 数据库 | PostgreSQL 16 + Spring Data JPA,使用 Flyway 进行 schema 版本控制 | | 缓存 / 限流 / 威胁状态 | Redis 7 | | PDF 报告 | Apache PDFBox | | 文档 | SpringDoc OpenAPI (Swagger UI) | | 仪表盘 | 单页原生 JS + SSE —— 无前端构建步骤 | | 基础设施 | Docker, Docker Compose, GitHub Actions CI, 纯 EC2 部署脚本 | ## 快速开始(本地) ### 前置条件 - Java 17+ - Docker 和 Docker Compose - 一个 OpenAI 和/或 Anthropic 的 API key ### 1. 克隆并配置 ``` git clone https://github.com/pracda/llm-api-gateway.git cd llm-api-gateway cp .env.example .env # 编辑 .env 并添加你的 API keys,以及一个真实的 JWT_SECRET(32+ 个字符) ``` ### 2. 启动所有服务 ``` docker compose --env-file .env -f docker/docker-compose.yml up -d --build ``` 这会构建应用镜像并一起启动 Postgres、Redis 和网关。Flyway 会在首次启动时自动迁移 schema。 ### 3. 打开仪表盘 访问 `http://localhost:8080/admin-dashboard.html`。使用 `admin` 和你 `.env` 文件中的 `ADMIN_PASSWORD` 值登录(如果未设置,默认为 `admin123` —— 在进行任何实际操作之前请更改此项)。 ### 4. 或者直接探索 API Swagger UI: `http://localhost:8080/swagger-ui` ## 部署到 AWS `deploy/aws/` 包含了一个最简单、无冗余的到单一 EC2 实例的部署(没有 Elastic Beanstalk,没有 Auto Scaling Group,没有负载均衡器 —— 这是有意为之,以保持演示/作品集部署的成本接近于零): ``` cd deploy/aws ./up.ps1 # provisions a fresh EC2 instance, generates secrets, boots the full stack ./status.ps1 # checks health of the running deployment ./down.ps1 # tears it down completely ``` `up.ps1` 动态解析最新的 Amazon Linux AMI 和默认的 VPC/subnet,为除你的 LLM 提供商密钥(从你本地的 `.env` 中拉取)之外的所有内容生成全新的随机密钥,并在完成后打印仪表盘 URL 和管理员密码。 ## API 参考 所有管理员 endpoint 都需要 `ROLE_ADMIN`;`/chat` 和 `/chat/stream` 明确需要 API key(仅凭 JWT 会返回 403)。 **认证与自助服务** | 方法 | Endpoint | 认证 | 描述 | |---|---|---|---| | POST | `/api/v1/auth/register` | 公开 | 创建账户 | | POST | `/api/v1/auth/token` | 公开 | 用凭证换取 JWT | | GET | `/api/v1/users/me` | JWT | 你的账户信息 | | POST | `/api/v1/users/me/api-keys` | JWT | **自助服务** —— 签发你自己的 TRIAL 级别的 API key,无需管理员。每个账户一个;再次调用你会得到一个 `400`,指引你使用下面的列表 endpoint,而不是获得第二个 key | | GET | `/api/v1/users/me/api-keys` | JWT | 你已签发的 API key(已掩码) | 注册并获取可用的 API key 只需要两次调用,无需管理员介入:`POST /api/v1/auth/register` → `POST /api/v1/auth/token`(获取 JWT)→ `POST /api/v1/users/me/api-keys`(获取你的 key,仅显示一次)。第一次自助调用也会静默地为你创建一个个人组织 —— 除非你超出了 TRIAL 级别并向管理员要求更多,否则你永远不需要考虑组织的问题。 **网关** | 方法 | Endpoint | 认证 | 描述 | |---|---|---|---| | POST | `/api/v1/chat` | API key | 发送 prompt,获取完整响应 | | POST | `/api/v1/chat/stream` | API key | 相同,但进行逐 token 的流式传输 (SSE) | | GET | `/api/v1/models` | JWT 或 API key | 列出每个提供商的 `default` 模型和 `allowed` 允许列表,以便客户端可以发现有效的 `model` 值,而不是靠猜测并得到 `400` | | GET | `/api/v1/health` | JWT | 经过身份验证的健康检查 | | GET | `/actuator/health` | 公开 | 简单的存活检查(由 Docker/部署脚本使用) | `GET /api/v1/models` 返回示例: ``` { "OPENAI": { "default": "gpt-4o-mini", "allowed": ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"] }, "ANTHROPIC": { "default": "claude-haiku-4-5-20251001", "allowed": ["claude-haiku-4-5-20251001", "claude-sonnet-5", "claude-opus-4-8"] } } ``` 提供商的 `allowed` 数组为空意味着当前不接受它的任何显式 `model` 字符串 —— 请求仍然可以完全省略 `model` 以回退到 `default`。与 `/chat` 不同,此 endpoint 仅接受 JWT(不需要 API key),因为它只需要*任何*已验证的主体。 **管理员 —— 仪表盘概览** | 方法 | Endpoint | 描述 | |---|---|---| | GET | `/api/v1/admin/logs` | 所有用户的分页审计日志 | | GET | `/api/v1/admin/logs/{userId}` | 单个用户的审计日志 | | GET | `/api/v1/admin/stats` | 总体使用统计 | | GET | `/api/v1/admin/stats/today` | 今日摘要 | | GET | `/api/v1/admin/users` | 按请求数排名的顶级用户 | | GET | `/api/v1/admin/stream` | 仪表盘的实时 SSE 推送 | | GET | `/api/v1/admin/risk-rankings` | 按用户的风险评分,最高者优先 | | GET | `/api/v1/admin/attack-timeline` | 按小时统计的被拦截请求数 | **管理员 —— 组织、成员与 API key** | 方法 | Endpoint | 描述 | |---|---|---| | POST | `/api/v1/admin/organizations` | 创建组织 | | GET | `/api/v1/admin/organizations` | 列出组织 | | GET | `/api/v1/admin/organizations/{id}` | 组织详情 —— 成员的 key | | POST | `/api/v1/admin/organizations/{id}/members` | 添加成员(自动配置账户) | | GET | `/api/v1/admin/organizations/{id}/logs` | 限定于此组织的审计日志 | | GET | `/api/v1/admin/organizations/{id}/trend` | 按天分桶的请求/token/警报趋势 | | GET | `/api/v1/admin/organizations/{id}/report` | 可下载的每周/每月 PDF 报告 | | POST | `/api/v1/admin/organizations/{id}/api-keys` | 为成员签发 API key | | POST | `/api/v1/admin/api-keys/{id}/suspend` | 暂时挂起 key | | POST | `/api/v1/admin/api-keys/{id}/resume` | 恢复已挂起的 key | | DELETE | `/api/v1/admin/api-keys/{id}` | 永久撤销 key | **管理员 —— 警报与 IP 黑名单** | 方法 | Endpoint | 描述 | |---|---|---| | GET | `/api/v1/admin/alerts` | 分页的安全警报 | | POST | `/api/v1/admin/alerts/{id}/acknowledge` | 确认警报 | | GET | `/api/v1/admin/ip-blocks` | 出被封锁的 IP | | POST | `/api/v1/admin/ip-blocks` | 封锁 IP | | DELETE | `/api/v1/admin/ip-blocks/{ip}` | 解除对 IP 的封锁 | **管理员 —— 按用户的证据追踪** | 方法 | Endpoint | 描述 | |---|---|---| | POST | `/api/v1/admin/users/{username}/unlock` | 清除自动锁定 | | GET | `/api/v1/admin/users/{username}/activity` | 证据追踪 —— 意图细分、趋势、警报、key | | POST | `/api/v1/admin/users/{username}/block` | 永久封锁 —— 禁用账户,撤销所有 key | ## 请求示例 完全自助服务 —— 无需管理员介入: ``` # 1. 注册 curl -X POST http://localhost:8080/api/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"prasiddha","password":"yourpassword"}' # 2. 登录以获取 JWT JWT=$(curl -s -X POST http://localhost:8080/api/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"username":"prasiddha","password":"yourpassword"}' | jq -r .token) # 3. 生成你自己的 TRIAL-tier API key — 仅显示一次,请立即保存 API_KEY=$(curl -s -X POST http://localhost:8080/api/v1/users/me/api-keys \ -H "Authorization: Bearer $JWT" | jq -r .apiKey) # 4. 使用你的 key 调用 gateway curl -X POST http://localhost:8080/api/v1/chat \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "OPENAI", "model": "gpt-4o-mini", "userMessage": "Explain prompt injection in one paragraph." }' ``` 需要超出 TRIAL 的限制?管理员仍然可以直接通过 `POST /api/v1/admin/organizations/{id}/api-keys` 签发 STANDARD/ENTERPRISE 级别的 key —— 请参阅上面的 API 参考。 ## OWASP LLM 应用 Top 10 —— 覆盖范围 | 风险 | 状态 | 实现方式 | |---|---|---| | Prompt 注入 | 已覆盖 | `InputScanService` —— 模式 + 编码 payload 检测,越狱评分 | | 不当的输出处理 | 已覆盖 | `OutputScanService` —— XSS/SQLi/shell-injection 回显拦截 | | 敏感信息泄露 | 已覆盖 | Prompts/responses 永不存储(仅哈希);输入/输出 PII 脱敏 | | System Prompt 泄露 | 已覆盖 | Canary-token 注入 + 检测,自动撤销 key | | 无限制消耗 / 模型 DoS | 已覆盖 | 多级限流(包括 auth endpoint 上基于 IP 的限制),按 key 的 token 上限,按 key 的每日 $ 预算强制执行,提取量检测 | | 过度代理 | 部分适用 | 此网关中没有工具调用;通过分级 key 和手动封锁限制影响范围 | | 供应链 / 数据投毒 / 错误信息 | 超出范围 | 不适用于不训练或托管模型的代理网关 | ## 测试 需要本地安装 Maven(未签入 wrapper)。主要测试套件针对内存中的 H2 数据库运行,无需 Docker 服务: ``` mvn test -Dtest='!PostgresIntegrationTest' ``` 跨单元和 MockMvc 集成套件的 71 个测试 —— 输入/输出扫描、威胁检测和锁定逻辑、API key 生命周期、组织/成员配置、意图分类、PDF 报告生成以及 auth 边界检查(仅限 JWT 与通过 API key 访问 `/chat` 的对比)。 一个单独的基于 Testcontainers 的测试验证了应用程序针对**真实的** PostgreSQL 和 Redis(而不是 H2 由 Hibernate 生成的 schema)—— 这才是真正端到端测试 Flyway 迁移的部分,包括 H2 的 `create-drop` 模式可能会静默掩盖的 Postgres 特定列类型。需要本地运行 Docker: ``` mvn test -Dtest=PostgresIntegrationTest ``` ## 项目结构 ``` src/main/java/com/prasiddha/gateway/ ├── controller/ # REST endpoints — chat, auth, users, and 5 focused admin controllers ├── service/ # Business logic — scanning, rate limiting, threat detection, reports ├── security/ # JWT + API-key auth filters ├── proxy/ # OpenAI / Anthropic provider clients ├── model/ # Entities, requests, responses └── repository/ # Spring Data JPA repositories src/main/resources/ ├── db/migration/ # Flyway schema migrations └── static/ # Admin dashboard (single HTML file, no build step) deploy/aws/ # Plain-EC2 up/down/status PowerShell scripts docker/ # Dockerfile + docker-compose.yml (app + Postgres + Redis) ``` ## 许可证 PRASIDDHA
标签:API网关, DLL 劫持, JWT认证, Spring Boot, 域名枚举, 大语言模型, 版权保护, 请求拦截