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 是必填参数;角色名称得统一成 user 和 assistant。
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 错误。
排查步骤:
- 检查请求头格式:必须是
Authorization: Bearer <key>,注意 Bearer 后面有空格 - 验证 Key 是否在
VALID_KEY_HASHES列表中 - 检查 Key 有没有过期(看
keys.js配置) - 查看网关日志确认收到的 Key 值(只在调试模式)
8.2 协议不兼容(400/422)
现象:Claude API 返回 invalid_request_error。
原因:通常是字段映射错了或缺少必填参数。
解决方案:
- 确认
max_tokens已设置(Anthropic 必填) - 检查
messages数组中角色名是不是user或assistant - 验证
system字段有没有独立提取(不应该出现在messages中) - 开启
DEBUG_MODE打印完整请求体对比官方文档
8.3 超时与重试
现象:请求长时间没响应后返回 ETIMEDOUT。
解决方案:
- 检查 Axios 的
timeout配置够不够(建议 60 秒以上) - 确认重试逻辑只对幂等错误生效(避免重复扣费)
- 查看熔断器状态(
breaker.stats),要是频繁熔断得检查 Claude API 可用性 - 用
AbortController支持客户端主动取消请求
8.4 流式响应中断
现象:流式响应传到一半停了。
原因:通常是客户端断连或网关进程崩了。
解决方案:
- 监听
response.data.on('error')和req.on('close')事件 - 客户端断连时主动销毁上游连接(调用
response.data.destroy()) - 用 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 仓库拿到可运行版本,然后根据你的实际需求做定制扩展。
更多推荐

所有评论(0)