Node.js 搭建 Claude API 网关:鉴权、转发与生产实践完全指南一、为什么需要自建 AI 接口网关

Node.js 搭建 Claude API 网关:鉴权、转发与生产实践完全指南

一、为什么需要自建 AI 接口网关

市面上其实已经有 LiteLLM、sdcb/chats、CC-Switch 这些第三方网关工具,但说实话它们的局限性还是挺明显的:配置不够灵活、很难嵌入到你现有的业务系统里、对鉴权和限流的控制也不够精细。如果你的团队需要深度定制,用 Node.js 自己搭一个 AI 接口网关能带来三个核心好处。

成本控制:通过统一网关接入多个 AI 模型(Claude、GPT、国产大模型),可以实现动态路由和降级策略。比如说,把那些不太重要的请求丢给成本更低的模型,高优先级的请求再分配给 Claude Opus,这样整体 Token 消耗就能降下来。另外自建网关还能加个请求缓存,遇到相同的 prompt 直接返回缓存结果,避免重复调用浪费钱。

数据安全:第三方网关得把你的 API Key 和业务数据托管给外部服务,这其实存在泄露风险。自建网关的话,所有敏感信息都留在你自己的内网里,通过自己的鉴权机制控制访问权限,这对金融、医疗这些有合规要求的行业来说特别重要。

定制化能力:企业级场景往往需要精细的流量管理,像是按用户维度限流、动态调整超时参数、记录完整调用链路用于审计这些需求,在通用工具里很难实现,但在自建网关中只要写个中间件就能搞定。

这篇文章会从零开始实现一个生产级的 Node.js Claude API 网关,覆盖鉴权、协议转换、流式响应、错误处理、监控告警等完整链路,并且给出可以直接跑起来的源码示例。

二、技术选型与分层架构

技术栈选择:我们用 Express 做 Web 框架(当然也可以选性能更高的 Fastify),用 Axios 处理 HTTP 请求(它支持请求拦截、超时控制、连接池复用),可以考虑集成 Redis 来实现分布式限流和缓存。

分层架构设计

客户端请求 ↓ [ 鉴权层 ] ← API Key 验证、JWT 解析、频率限制 ↓ [ 路由层 ] ← 根据请求参数选择目标模型 ↓ [ 转发层 ] ← 协议转换、HTTP 调用、流式响应处理 ↓ [ 日志层 ] ← 结构化日志、性能指标采集 ↓ Claude API / 其他 AI 模型

每层职责都是独立的,通过 Express 中间件机制串起来。鉴权层拦掉那些无效请求,路由层决定转发到哪个目标,转发层处理具体的 API 调用,日志层记录完整链路信息。这种分层设计方便后续扩展多模型支持或者接入企业内部系统。

三、鉴权模块实现

3.1 API Key 验证中间件

最常见的鉴权方式就是在请求头里带上 API Key,网关验证通过后放行。代码实现如下:

// middlewares/auth.js const crypto = require('crypto'); // 从环境变量或数据库加载有效的 API Key(已哈希处理) const VALID_KEY_HASHES = [ crypto.createHash('sha256').update(process.env.API_KEY_1).digest('hex'), // 支持多个 Key ]; function authMiddleware(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing or invalid Authorization header' }); } const providedKey = authHeader.slice(7); const providedHash = crypto.createHash('sha256').update(providedKey).digest('hex'); if (!VALID_KEY_HASHES.includes(providedHash)) { return res.status(403).json({ error: 'Invalid API key' }); } next(); } module.exports = authMiddleware;

安全要点:千万别在代码里硬编码明文 Key,通过环境变量注入;存储的时候用 SHA-256 哈希,验证时比对哈希值而不是明文;支持多 Key 管理,这样方便轮换和权限分级。

3.2 频率限制(三级限流)

express-rate-limit实现基于 IP、用户、Key 的多级限流:

// middlewares/rateLimit.js const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const redisClient = new Redis(process.env.REDIS_URL); // IP 级限流:防止单 IP 暴力请求 const ipLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:ip:' }), windowMs: 60 * 1000, // 1 分钟 max: 100, // 最多 100 次请求 message: { error: 'Too many requests from this IP' }, }); // Key 级限流:按 API Key 控制配额 const keyLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:key:' }), windowMs: 60 * 60 * 1000, // 1 小时 max: 1000, keyGenerator: (req) => req.headers.authorization, message: { error: 'API key quota exceeded' }, }); module.exports = { ipLimiter, keyLimiter };

三级限流策略:全局 IP 限流防攻击,Key 限流控制单个客户配额,还可以加个用户维度限流(需要结合 JWT 解析用户 ID)。Redis 作为共享存储,这样多实例部署时也能实现分布式限流。

3.3 Key 轮换与优雅降级

生产环境需要定期轮换 API Key,别让它长期暴露在外面。实现方案如下:

// config/keys.js const KEYS_CONFIG = [ { hash: '...', expiresAt: '2024-12-31', priority: 1 }, { hash: '...', expiresAt: '2025-06-30', priority: 2 }, // 新 Key ]; function validateKey(providedHash) { const now = new Date(); const validKeys = KEYS_CONFIG .filter(k => new Date(k.expiresAt) > now) .sort((a, b) => a.priority - b.priority); return validKeys.some(k => k.hash === providedHash); }

配置好过期时间和优先级,网关会自动过滤掉过期的 Key。当主 Key 快过期时,提前签发新 Key 并降低旧 Key 优先级,客户端就能无缝切换。

四、请求转发与协议适配

4.1 OpenAI → Anthropic Messages API 格式转换

很多客户端用 OpenAI SDK 格式发送请求,网关需要转成 Anthropic Messages API 格式。核心字段映射如下:

// utils/protocolAdapter.js function openaiToAnthropic(openaiRequest) { const { model, messages, temperature, max_tokens, stream } = openaiRequest; // 提取 system prompt const systemMessage = messages.find(m => m.role === 'system'); const conversationMessages = messages.filter(m => m.role !== 'system'); return { model: model.replace('gpt-', 'claude-'), // 简单映射,实际需更精细 max_tokens: max_tokens || 4096, temperature: temperature || 1.0, system: systemMessage?.content || '', messages: conversationMessages.map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content, })), stream: stream || false, }; }

关键差异点:Anthropic 用独立的system字段而不是混到messages里;max_tokens是必填参数;角色名称得统一成userassistant

4.2 HTTP 客户端配置与重试

用 Axios 配置超时、连接池、指数退避重试:

// services/claudeClient.js const axios = require('axios'); const axiosRetry = require('axios-retry'); const client = axios.create({ baseURL: 'https://api.anthropic.com', timeout: 60000, // 60 秒超时 headers: { 'anthropic-version': '2023-06-01', 'x-api-key': process.env.CLAUDE_API_KEY, }, maxSockets: 50, // 连接池大小 }); axiosRetry(client, { retries: 3, retryDelay: axiosRetry.exponentialDelay, // 指数退避 retryCondition: (error) => { // 仅对网络错误和 429/500 重试 return axiosRetry.isNetworkOrIdempotentRequestError(error) || [429, 500, 502, 503].includes(error.response?.status); }, }); module.exports = client;

生产配置要点:超时时间得大于模型响应时间(Claude 流式响应可能持续几十秒);连接池避免频繁建立 TCP 连接;只对幂等错误重试,避免重复扣费。

4.3 流式响应处理

Claude API 支持 SSE(Server-Sent Events)流式返回,网关需要透传给客户端:

// routes/chat.js router.post('/v1/chat/completions', authMiddleware, async (req, res) => { try { const anthropicPayload = openaiToAnthropic(req.body); if (anthropicPayload.stream) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const response = await client.post('/v1/messages', anthropicPayload, { responseType: 'stream', }); response.data.pipe(res); response.data.on('error', (err) => { console.error('Stream error:', err); res.end(); }); } else { const response = await client.post('/v1/messages', anthropicPayload); res.json(response.data); } } catch (error) { handleError(error, res); } });

流式响应得设置正确的 HTTP 头,用pipe方法直接转发 Claude 的数据流。注意监听error事件,避免客户端断连时网关进程崩掉。

4.4 错误处理与熔断器

opossum库实现熔断器,防止 Claude API 挂了时网关还在持续发送无效请求:

// services/circuitBreaker.js const CircuitBreaker = require('opossum'); const breaker = new CircuitBreaker(async (payload) => { return await client.post('/v1/messages', payload); }, { timeout: 30000, // 30 秒超时触发熔断 errorThresholdPercentage: 50, // 错误率超过 50% 开启熔断 resetTimeout: 10000, // 10 秒后尝试恢复 }); breaker.on('open', () => console.warn('Circuit breaker opened')); breaker.on('halfOpen', () => console.info('Circuit breaker half-open')); module.exports = breaker;

熔断器开启后,请求直接返回错误而不实际调 API,避免雪崩效应。半开状态时放行部分请求探测服务恢复情况。

五、日志、监控与调试

5.1 结构化日志配置

winston记录每个请求的完整链路信息:

// config/logger.js const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }), ], }); // 日志中间件:记录请求 ID、耗时、状态码 function loggerMiddleware(req, res, next) { req.id = crypto.randomUUID(); const start = Date.now(); res.on('finish', () => { logger.info({ requestId: req.id, method: req.method, path: req.path, statusCode: res.statusCode, duration: Date.now() - start, userAgent: req.headers['user-agent'], }); }); next(); }

生产环境必须记录requestId用于全链路追踪,记录duration用于性能分析。别记完整请求体(可能包含敏感数据),只记元信息就行。

5.2 性能指标采集

计算 P95 延迟和错误率,可以接入 Prometheus:

// utils/metrics.js const promClient = require('prom-client'); const requestDuration = new promClient.Histogram({ name: 'gateway_request_duration_seconds', help: 'Duration of gateway requests', labelNames: ['method', 'path', 'status'], buckets: [0.1, 0.5, 1, 2, 5, 10], }); const errorCounter = new promClient.Counter({ name: 'gateway_errors_total', help: 'Total number of errors', labelNames: ['type'], }); // 在日志中间件中调用 requestDuration.observe({ method, path, status: res.statusCode }, duration / 1000); if (res.statusCode >= 400) { errorCounter.inc({ type: res.statusCode >= 500 ? 'server' : 'client' }); }

Prometheus 采集后可以用 Grafana 做可视化,设置告警规则(比如 P95 延迟超过 5 秒或错误率超过 5% 时触发通知)。

5.3 本地调试模式

开发环境需要打印完整请求和响应体,通过环境变量控制:

if (process.env.DEBUG_MODE === 'true') { client.interceptors.request.use(req => { console.log('[DEBUG] Request:', JSON.stringify(req.data, null, 2)); return req; }); client.interceptors.response.use(res => { console.log('[DEBUG] Response:', JSON.stringify(res.data, null, 2)); return res; }); }

生产环境一定要关掉这个开关,避免日志泄露用户数据。

六、部署与优化

6.1 Docker 多阶段构建

用多阶段 Dockerfile 减小镜像体积:

# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # 运行阶段 FROM node:18-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY . . EXPOSE 3000 CMD ["node", "server.js"]

最终镜像只包含生产依赖,体积通常小于 150MB。用 Alpine 基础镜像能进一步优化。

6.2 环境变量管理

提供.env.example模板,用dotenv加载:

# .env.example PORT=3000 CLAUDE_API_KEY=sk-ant-xxx API_KEY_1=your-gateway-key-1 REDIS_URL=redis://localhost:6379 LOG_LEVEL=info DEBUG_MODE=false

生产部署时通过 Kubernetes ConfigMap 或 Docker Compose 环境变量注入,别把真实密钥提交到代码仓库。

6.3 Nginx 负载均衡

网关无状态设计支持水平扩展,用 Nginx 分发流量:

upstream gateway_backend { least_conn; server gateway-1:3000; server gateway-2:3000; server gateway-3:3000; } server { listen 80; location / { proxy_pass http://gateway_backend; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

least_conn策略把请求分发到连接数最少的实例,适合长连接场景(比如流式响应)。

6.4 成本优化:请求缓存

对相同 prompt 的重复请求用 Redis 缓存:

// middlewares/cache.js const redis = require('ioredis'); const client = new redis(process.env.REDIS_URL); async function cacheMiddleware(req, res, next) { if (req.body.stream) return next(); // 流式请求不缓存 const cacheKey = `cache:${crypto.createHash('sha256').update(JSON.stringify(req.body)).digest('hex')}`; const cached = await client.get(cacheKey); if (cached) { return res.json(JSON.parse(cached)); } // 拦截响应并缓存 const originalJson = res.json.bind(res); res.json = (data) => { client.setex(cacheKey, 3600, JSON.stringify(data)); // 缓存 1 小时 return originalJson(data); }; next(); }

适用于知识问答、文档摘要这些幂等场景。缓存命中率达到 30% 就能显著降低 Token 消耗。

七、完整项目示例

7.1 目录结构

gateway/ ├── src/ │ ├── config/ │ │ ├── logger.js # Winston 日志配置 │ │ └── keys.js # API Key 管理 │ ├── middlewares/ │ │ ├── auth.js # 鉴权中间件 │ │ ├── rateLimit.js # 限流中间件 │ │ └── cache.js # 缓存中间件 │ ├── routes/ │ │ └── chat.js # 聊天接口路由 │ ├── services/ │ │ ├── claudeClient.js # Axios 客户端 │ │ └── circuitBreaker.js # 熔断器 │ └── utils/ │ ├── protocolAdapter.js # 协议转换 │ └── metrics.js # 指标采集 ├── tests/ │ └── auth.test.js # Jest 单元测试 ├── .env.example # 环境变量模板 ├── Dockerfile # 容器镜像 ├── docker-compose.yml # 本地部署编排 ├── package.json └── server.js # 入口文件

7.2 本地运行步骤

# 1. 克隆项目 git clone https://github.com/your-org/node-claude-gateway.git cd node-claude-gateway # 2. 安装依赖 npm install # 3. 配置环境变量 cp .env.example .env # 编辑 .env 填入 Claude API Key # 4. 启动 Redis(使用 Docker) docker run -d -p 6379:6379 redis:alpine # 5. 启动网关 npm start # 6. 测试请求 curl -X POST http://localhost:3000/v1/chat/completions \ -H "Authorization: Bearer your-gateway-key-1" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-sonnet", "messages": [{"role": "user", "content": "Hello"}], "stream": false }'

7.3 单元测试示例

// tests/auth.test.js const request = require('supertest'); const app = require('../server'); describe('Auth Middleware', () => { test('should reject request without auth header', async () => { const res = await request(app).post('/v1/chat/completions'); expect(res.statusCode).toBe(401); }); test('should accept valid API key', async () => { const res = await request(app) .post('/v1/chat/completions') .set('Authorization', 'Bearer valid-test-key') .send({ model: 'claude-3-sonnet', messages: [] }); expect(res.statusCode).not.toBe(401); }); });

用 Jest 覆盖鉴权、限流、协议转换这些核心逻辑,在 CI/CD 流程中自动跑起来。

八、常见问题排查

8.1 鉴权失败(401/403)

现象:客户端收到Invalid API key错误。

排查步骤

  1. 检查请求头格式:必须是Authorization: Bearer <key>,注意 Bearer 后面有空格
  2. 验证 Key 是否在VALID_KEY_HASHES列表中
  3. 检查 Key 有没有过期(看keys.js配置)
  4. 查看网关日志确认收到的 Key 值(只在调试模式)

8.2 协议不兼容(400/422)

现象:Claude API 返回invalid_request_error

原因:通常是字段映射错了或缺少必填参数。

解决方案

  1. 确认max_tokens已设置(Anthropic 必填)
  2. 检查messages数组中角色名是不是userassistant
  3. 验证system字段有没有独立提取(不应该出现在messages中)
  4. 开启DEBUG_MODE打印完整请求体对比官方文档

8.3 超时与重试

现象:请求长时间没响应后返回ETIMEDOUT

解决方案

  1. 检查 Axios 的timeout配置够不够(建议 60 秒以上)
  2. 确认重试逻辑只对幂等错误生效(避免重复扣费)
  3. 查看熔断器状态(breaker.stats),要是频繁熔断得检查 Claude API 可用性
  4. AbortController支持客户端主动取消请求

8.4 流式响应中断

现象:流式响应传到一半停了。

原因:通常是客户端断连或网关进程崩了。

解决方案

  1. 监听response.data.on('error')req.on('close')事件
  2. 客户端断连时主动销毁上游连接(调用response.data.destroy()
  3. 用 PM2 或 Kubernetes 确保网关进程自动重启

九、进阶话题

9.1 多模型支持

扩展网关支持 Claude、GPT、Gemini 这些多模型,通过请求参数动态路由:

const MODEL_ENDPOINTS = { 'claude-': 'https://api.anthropic.com/v1/messages', 'gpt-': 'https://api.openai.com/v1/chat/completions', 'gemini-': 'https://generativelanguage.googleapis.com/v1/models', }; function selectEndpoint(model) { const prefix = Object.keys(MODEL_ENDPOINTS).find(p => model.startsWith(p)); return MODEL_ENDPOINTS[prefix]; }

每个模型用独立的协议适配器和客户端配置,统一通过网关对外暴露。

9.2 动态路由与 A/B 测试

根据用户 ID 或请求特征把流量分配到不同模型版本:

function abTestRouter(userId, models) { const hash = crypto.createHash('md5').update(userId).digest('hex'); const bucket = parseInt(hash.slice(0, 8), 16) % 100; return bucket < 50 ? models[0] : models[1]; // 50% 流量分配 }

适用于对比不同模型的效果或测试新版本网关的稳定性。

9.3 与 Kubernetes Ingress 集成

把网关部署成 Kubernetes Service,通过 Ingress 暴露:

apiVersion: v1 kind: Service metadata: name: gateway-service spec: selector: app: gateway ports: - port: 80 targetPort: 3000 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gateway-ingress spec: rules: - host: api.yourcompany.com http: paths: - path: / pathType: Prefix backend: service: name: gateway-service port: number: 80

配合 cert-manager 自动签发 HTTPS 证书,实现生产级暴露。

十、总结与资源

用 Node.js 搭建 Claude API 网关的核心要点:通过分层架构实现鉴权、协议转换、流式响应、错误处理的解耦;用 Redis 支持分布式限流和缓存;配置熔断器和重试机制保障稳定性;接入结构化日志和性能指标实现可观测性。

跟第三方工具比起来,自建网关在成本控制(缓存、动态路由)、数据安全(内网部署)、定制化能力(精细鉴权、多模型路由)方面优势明显,适合有一定技术储备而且对灵活性要求比较高的团队。

参考资源

  • Anthropic Messages API 官方文档:https://docs.anthropic.com/claude/reference
  • Express 中间件开发指南:Writing middleware for use in Express apps · Express.js
  • Axios 高级配置:Request config | axios | Promise based HTTP client
  • opossum 熔断器库:GitHub - nodeshift/opossum: Node.js circuit breaker - fails fast ⚡️ · GitHub

本文提供的完整项目代码已经开源了,可以去 GitHub 仓库拿到可运行版本,然后根据你的实际需求做定制扩展。