阶段五:实战项目
API网关是微服务架构的"前门"——所有外部请求都经过它。它统一处理认证、限流、路由、日志等横切关注点,让后端服务专注业务逻辑。本课实现一个功能完整的API网关,涵盖路由转发、认证中间件、限流、熔断、日志和健康检查。
| 功能 | 说明 | 实现方式 |
|---|---|---|
| 路由转发 | 将请求路由到正确的后端服务 | 路径匹配+代理 |
| 认证授权 | 统一验证身份和权限 | JWT验证中间件 |
| 限流 | 防止滥用和过载 | 令牌桶/滑动窗口 |
| 熔断 | 防止故障级联 | 断路器模式 |
| 负载均衡 | 分发请求到多个实例 | 轮询/权重/最少连接 |
| 日志监控 | 请求追踪和指标收集 | 请求ID+日志中间件 |
| 协议转换 | REST↔gRPC等 | grpc-gateway |
| 缓存 | 减少重复请求 | 响应缓存中间件 |
// api-gateway.js - 完整API网关
const express = require('express');
const http = require('http');
const crypto = require('crypto');
const app = express();
// ===== 配置 =====
const SERVICES = {
'user-service': { url: 'http://localhost:3001', prefix: '/api/users', healthCheck: '/health' },
'product-service': { url: 'http://localhost:3002', prefix: '/api/products', healthCheck: '/health' },
'order-service': { url: 'http://localhost:3003', prefix: '/api/orders', healthCheck: '/health' },
};
// ===== 请求ID中间件 =====
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
res.setHeader('X-Request-ID', req.requestId);
res.setHeader('X-Gateway', 'api-gateway/1.0');
next();
});
// ===== 日志中间件 =====
const requestLogs = [];
app.use((req, res, next) => {
const start = Date.now();
const entry = { requestId: req.requestId, method: req.method, path: req.path, start, userId: null };
res.on('finish', () => {
entry.duration = Date.now() - start;
entry.statusCode = res.statusCode;
entry.userId = req.userId || null;
requestLogs.push(entry);
console.log(`→ ${req.method} ${req.path} ${res.statusCode} ${entry.duration}ms [${req.requestId}]`);
});
next();
});
// ===== 认证中间件 =====
const JWT_SECRET = 'gateway-secret-key';
function verifyToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
if (payload.exp && payload.exp < Date.now() / 1000) return null;
return payload;
} catch { return null; }
}
function authenticate(req, res, next) {
const publicPaths = ['/api/auth/login', '/api/auth/register', '/health', '/api/products'];
if (publicPaths.some(p => req.path.startsWith(p) && req.method === 'GET')) return next();
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ success: false, error: { code: 'AUTH_MISSING', message: '缺少认证令牌' } });
}
const payload = verifyToken(authHeader.substring(7));
if (!payload) {
return res.status(401).json({ success: false, error: { code: 'AUTH_INVALID', message: '令牌无效或已过期' } });
}
req.userId = payload.sub;
req.userRole = payload.role;
next();
}
app.use(authenticate);
// ===== 限流中间件 =====
const rateLimits = new Map();
function rateLimit(options = {}) {
const { windowMs = 60000, maxRequests = 100 } = options;
return (req, res, next) => {
const key = req.userId || req.ip;
const now = Date.now();
let record = rateLimits.get(key);
if (!record || now - record.windowStart > windowMs) {
record = { count: 0, windowStart: now };
rateLimits.set(key, record);
}
record.count++;
const remaining = Math.max(0, maxRequests - record.count);
res.setHeader('X-RateLimit-Limit', maxRequests.toString());
res.setHeader('X-RateLimit-Remaining', remaining.toString());
if (record.count > maxRequests) {
res.setHeader('Retry-After', Math.ceil((windowMs - (now - record.windowStart)) / 1000).toString());
return res.status(429).json({ success: false, error: { code: 'RATE_LIMIT', message: '请求频率超限' } });
}
next();
};
}
app.use(rateLimit({ windowMs: 60000, maxRequests: 200 }));
// ===== 熔断器 =====
class CircuitBreaker {
constructor(name, options = {}) {
this.name = name;
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 30000;
this.state = 'CLOSED';
this.failureCount = 0;
this.lastFailureTime = null;
}
recordSuccess() { this.failureCount = 0; this.state = 'CLOSED'; }
recordFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) this.state = 'OPEN';
}
isAvailable() {
if (this.state === 'CLOSED') return true;
if (this.state === 'OPEN' && Date.now() - this.lastFailureTime >= this.timeout) {
this.state = 'HALF_OPEN';
return true;
}
return this.state !== 'OPEN';
}
getStatus() { return { name: this.name, state: this.state, failures: this.failureCount }; }
}
const breakers = {};
Object.keys(SERVICES).forEach(name => { breakers[name] = new CircuitBreaker(name); });
// ===== 路由代理 =====
function proxyRequest(req, res, targetUrl) {
const url = new URL(req.originalUrl, targetUrl);
const options = {
method: req.method,
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
headers: {
...req.headers,
host: url.host,
'x-request-id': req.requestId,
'x-user-id': req.userId || '',
'x-user-role': req.userRole || '',
},
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error(`代理错误 [${req.requestId}]:`, err.message);
if (!res.headersSent) {
res.status(502).json({ success: false, error: { code: 'BAD_GATEWAY', message: '上游服务不可用' } });
}
});
if (req.body) proxyReq.write(JSON.stringify(req.body));
proxyReq.end();
}
// 模拟后端服务(生产中这些是独立进程)
function createMockService(port, name, data) {
const service = express();
service.use(express.json());
service.get('/health', (req, res) => res.json({ status: 'healthy', service: name }));
service.get('/*', (req, res) => {
res.json({ success: true, data, meta: { service: name, requestId: req.headers['x-request-id'] } });
});
service.post('/*', (req, res) => {
res.status(201).json({ success: true, data: { id: Date.now(), ...req.body }, meta: { service: name } });
});
service.listen(port, () => console.log(` 📦 ${name} 运行在 :${port}`));
}
// 路由分发
app.use('/api/users', (req, res) => {
const breaker = breakers['user-service'];
if (!breaker.isAvailable()) return res.status(503).json({ success: false, error: { code: 'CIRCUIT_OPEN', message: '用户服务暂时不可用' } });
// 模拟代理(简化,直接返回模拟数据)
res.json({ success: true, data: [{ id: 1, name: '张三', email: 'zhang@example.com' }], meta: { service: 'user-service', requestId: req.requestId } });
});
app.use('/api/products', (req, res) => {
const breaker = breakers['product-service'];
if (!breaker.isAvailable()) return res.status(503).json({ success: false, error: { code: 'CIRCUIT_OPEN', message: '商品服务暂时不可用' } });
res.json({ success: true, data: [{ id: 1, name: '机械键盘', price: 599 }], meta: { service: 'product-service', requestId: req.requestId } });
});
app.use('/api/orders', (req, res) => {
const breaker = breakers['order-service'];
if (!breaker.isAvailable()) return res.status(503).json({ success: false, error: { code: 'CIRCUIT_OPEN', message: '订单服务暂时不可用' } });
res.json({ success: true, data: [{ id: 101, total: 798, status: 'completed' }], meta: { service: 'order-service', requestId: req.requestId } });
});
// ===== 网关管理端点 =====
app.get('/health', (req, res) => {
const services = {};
for (const [name, breaker] of Object.entries(breakers)) {
services[name] = breaker.getStatus();
}
res.json({ status: 'healthy', gateway: '1.0', services, uptime: process.uptime() });
});
app.get('/gateway/stats', (req, res) => {
const total = requestLogs.length;
const byService = {};
const byStatus = {};
requestLogs.slice(-1000).forEach(log => {
const service = log.path.split('/')[2] || 'unknown';
byService[service] = (byService[service] || 0) + 1;
const statusGroup = Math.floor(log.statusCode / 100) + 'xx';
byStatus[statusGroup] = (byStatus[statusGroup] || 0) + 1;
});
const avgDuration = total > 0 ? Math.round(requestLogs.slice(-100).reduce((s, l) => s + (l.duration || 0), 0) / Math.min(100, total)) : 0;
res.json({
total,
byService,
byStatus,
avgDurationMs: avgDuration,
circuits: Object.fromEntries(Object.entries(breakers).map(([n, b]) => [n, b.getStatus()])),
});
});
app.use(express.json());
app.listen(8080, () => {
console.log('🌉 API网关运行在 http://localhost:8080');
console.log(' 健康检查: http://localhost:8080/health');
console.log(' 统计信息: http://localhost:8080/gateway/stats');
});
# 启动网关
node api-gateway.js
# 1. 公开接口(无需认证)
curl -s http://localhost:8080/api/products | jq .
# 2. 受保护接口(需要认证)
curl -s http://localhost:8080/api/orders | jq .
# 3. 带Token访问
curl -s http://localhost:8080/api/orders \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.xxx" | jq .
{
"success": true,
"data": [{"id":101,"total":798,"status":"completed"}],
"meta": {"service":"order-service","requestId":"..."}
}
# 4. 健康检查
curl -s http://localhost:8080/health | jq .
# 5. 统计信息
curl -s http://localhost:8080/gateway/stats | jq .
| 模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 单一网关 | 所有请求经过一个网关 | 简单、统一 | 单点故障 |
| BFF网关 | 每类客户端一个网关 | 定制化 | 多实例维护 |
| 网格网关 | Sidecar代理模式 | 无单点、可观测 | 复杂度高 |
// BFF(Backend for Frontend)网关模式
// Web端BFF
const webBff = express();
webBff.get('/dashboard', async (req, res) => {
// 聚合多个服务的数据
const [user, orders, notifications] = await Promise.all([
fetch('http://user-service/api/users/me'),
fetch('http://order-service/api/orders/recent'),
fetch('http://notification-service/api/notifications/unread'),
]);
res.json({ user: user.data, orders: orders.data, notifications: notifications.data });
});
// 移动端BFF(更精简的响应)
const mobileBff = express();
mobileBff.get('/dashboard', async (req, res) => {
const [user, orders] = await Promise.all([
fetch('http://user-service/api/users/me?fields=id,name,avatar'),
fetch('http://order-service/api/orders/recent?limit=3&fields=id,total,status'),
]);
res.json({ user: user.data, recentOrders: orders.data });
});
为product-service配置3个实例,实现轮询负载均衡。提示:维护实例列表和当前索引。
在网关层添加LRU缓存,对GET请求的结果缓存30秒。写操作自动失效缓存。