🌐 从零构建高可用 API 网关:鉴权、路由、性能优化全解析
作者 :古渡蓝按
技术栈 :Spring Cloud Gateway + redis + Nacos + 自定义鉴权
技术栈 :微信公众号(深入浅出谈java)感觉本篇对你有帮助可以关注一下,会不定期更新知识和面试资料、技巧!!!
一、引言:为什么我们需要 API 网关?
在微服务架构中,随着服务数量的增加,直接暴露后端服务给客户端会带来诸多问题:
- ❌ 安全隐患:每个服务都要重复实现鉴权逻辑
- ❌ 路径混乱:客户端需要维护多个服务地址
- ❌ 重复代码:日志、监控、限流等横切关注点重复开发
- ❌ 升级困难:服务变更对客户端影响大
API 网关(API Gateway) 应运而生,它作为系统的统一入口,承担了 路由转发、安全控制、协议转换、流量治理 等核心职责。
本文将带你从零开始,搭建一个 支持动态路由、全局鉴权、路径重写、高并发 的生产级网关服务,并深入剖析其设计思路、调用链路与性能优化策略。
二、网关的核心作用与典型场景
1. 网关的五大核心能力
能力 | 说明 |
---|---|
✅ 路由转发 | 将请求按规则转发到对应微服务 |
✅ 协议转换 | HTTP → gRPC、WebSocket 等 |
✅ 安全控制 | 鉴权、防重放、防篡改 |
✅ 流量治理 | 限流、熔断、降级 |
✅ 监控审计 | 请求日志、调用链追踪、QPS 监控 |
2. 典型应用场景
- 统一入口 :所有请求走
/api/**
统一入口 - 安全加固:防止未授权访问、签名验证、防刷
- 灰度发布:根据 Header 路由到不同版本服务
- 前后端分离:解决跨域、路径代理
- API 聚合:合并多个接口返回(BFF 模式)
三、技术选型:为什么选择 Spring Cloud Gateway?
对比项 | Nginx | Zuul 1.x | Spring Cloud Gateway |
---|---|---|---|
性能 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
编程模型 | 配置化 | 同步阻塞 | 异步非阻塞(Netty) |
动态路由 | 需 reload | 支持 | 支持(可对接 Nacos) |
扩展性 | 有限 | 一般 | 高(Filter 机制) |
开发成本 | 低 | 低 | 中(Java 编写) |
适用场景 | 静态代理 | 旧项目 | 微服务网关首选 |
✅ 结论:SCG 基于 Reactor 和 Netty,性能高、扩展性强,是微服务网关的理想选择。
四、架构设计:整体架构与模块划分
graph TD A[Client] --> B[API Gateway] B --> C[Route: /pass/**] B --> D[Route: /auth/**] B --> E[Route: /file/**] C --> F[AuthGlobalFilter] F --> G[鉴权逻辑] G --> H{鉴权成功?} H -->|是| I[StripPrefix → 转发] H -->|否| J[返回 401] I --> K[Service A] I --> L[Service B]
核心模块
- 动态路由模块 :基于
RouteLocator
实现路径匹配 - 全局过滤器 :
GlobalFilter
实现鉴权、日志等 - 安全模块:自定义签名验证(HMAC-SHA256)
- 性能优化:GZIP、缓存、JVM 调优
五、核心实现:全局鉴权过滤器深度解析
1. 需求分析
- ✅ 支持
POST
请求体参与签名 - ✅ 支持
GET
请求参数透传 - ✅ 防重放攻击(timestamp + nonce)
- ✅ 签名验证失败返回
401
2. 关键代码:AuthGlobalFilter
java
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final Map<String, String> appSecrets = Map.of("APP_A_001", "your-secret-key-123");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 仅拦截 /pass/** 请求
if (!path.startsWith("/pass/")) {
return chain.filter(exchange);
}
// 读取并缓存请求体(关键!保证 body 可重复读)
return DataBufferUtils.join(request.getBody())
.flatMap(dataBuffer -> {
byte[] bodyBytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bodyBytes);
DataBufferUtils.release(dataBuffer);
String body = new String(bodyBytes, StandardCharsets.UTF_8);
exchange.getAttributes().put("cachedRequestBody", body);
// 重新包装 request,保证 body 能转发到下游
Flux<DataBuffer> cachedBodyFlux = Flux.defer(() ->
Mono.just(exchange.getResponse().bufferFactory().wrap(bodyBytes))
);
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return cachedBodyFlux;
}
};
ServerWebExchange newExchange = exchange.mutate().request(mutatedRequest).build();
// 执行鉴权
return performAuthentication(newExchange, body)
.then(Mono.defer(() -> chain.filter(newExchange)))
.onErrorResume(ex -> {
if (ex instanceof AuthenticationException) {
return onError(exchange.getResponse(), ex.getMessage(), HttpStatus.UNAUTHORIZED);
}
return onError(exchange.getResponse(), "Internal error", HttpStatus.INTERNAL_SERVER_ERROR);
});
})
.switchIfEmpty(chain.filter(exchange)); // GET 请求直接放行
}
private Mono<Void> performAuthentication(ServerWebExchange exchange, String body) {
ServerHttpRequest request = exchange.getRequest();
String appId = request.getHeaders().getFirst("X-Pass-AppId");
String timestampStr = request.getHeaders().getFirst("X-Pass-Timestamp");
String nonce = request.getHeaders().getFirst("X-Pass-Nonce");
String sign = request.getHeaders().getFirst("X-Pass-Sign");
// 省略参数校验...
String secret = appSecrets.get(appId);
if (secret == null) {
return onError(exchange.getResponse(), "Invalid AppId", HttpStatus.UNAUTHORIZED);
}
// 验证时间戳(5分钟内)
try {
long timestamp = Long.parseLong(timestampStr);
long now = Instant.now().getEpochSecond();
if (Math.abs(now - timestamp) > 300) {
return onError(exchange.getResponse(), "Timestamp expired", HttpStatus.UNAUTHORIZED);
}
} catch (NumberFormatException e) {
return onError(exchange.getResponse(), "Invalid timestamp", HttpStatus.UNAUTHORIZED);
}
// ✅ 使用 path + body + timestamp + nonce 生成签名
String contentToSign = request.getURI().getPath() + body + timestampStr + nonce;
String expectedSign = SignUtil.sign(contentToSign, secret);
if (!expectedSign.equalsIgnoreCase(sign)) {
return onError(exchange.getResponse(), "Invalid signature", HttpStatus.UNAUTHORIZED);
}
return Mono.empty();
}
private Mono<Void> onError(ServerHttpResponse response, String msg, HttpStatus status) {
response.setStatusCode(status);
response.getHeaders().add("Content-Type", "application/json");
String body = String.format("{\"code\": %d, \"message\": \"%s\"}", status.value(), msg);
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8))));
}
@Override
public int getOrder() {
return -1; // 优先执行
}
}
3. 关键设计点
问题 | 解决方案 |
---|---|
❓ POST 请求体只能读一次 |
DataBufferUtils.join() 缓存 body |
❓ 缓存后如何转发到下游 | ServerHttpRequestDecorator 包装 request |
❓ GET 请求如何处理 |
switchIfEmpty(chain.filter(exchange)) 直接放行 |
❓ 鉴权失败如何中断 | onErrorResume 返回错误响应 |
六、调用链路:一次请求的完整旅程
以 POST /pass/test/create
为例:

详细请求路径分析
- 请求进入网关
↓ - RoutePredicateHandlerMapping:匹配路由
↓ - FilteringWebHandler:执行 GlobalFilter 和 GatewayFilter
↓ - 路由转发到目标地址(如 http://www.baidu.com/order/16)
我们一步步来看:
🔹 步骤 1:路由匹配(Route Matching)
-
网关会从所有可用的
RouteDefinition
中查找匹配的路由。 -
你的两个路由源都会被加载:
-
来自
CustomRouteConfig
:Path=/pass/order/**
→http://www.baidu.com
- 来自
application.yml
:Path=/pass/**
→http://localhost:8080
- 来自
-
由于
/pass/order/**
是更具体的路径,它会优先匹配(前提是order
设置合理)。 -
匹配成功后,生成一个Route对象,包含:
ID: `order_route` URI: `http://www.baidu.com` Predicates: `Path=/pass/order/**` Filters: (如果有)
🔹 步骤 2:执行 GlobalFilter
(AuthGlobalFilter)
-
匹配路由后,网关进入过滤器链。
-
AuthGlobalFilter
是GlobalFilter
,它的filter()
方法会被调用 -
✅ 此时,请求路径
/pass/order/16
被用于路由匹配 ,但CustomRouteConfig
本身 不会主动收到这个请求信息。
java
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath(); // → "/pass/order/16"
...
}
✅ 在这里,你可以获取到完整的请求信息,包括:
- 路径:
/pass/order/16
- Header:
X-Pass-AppId
,X-Pass-Sign
等 - 方法:GET/POST
- 查询参数等
👉 这是你做鉴权的正确位置。
🔹 步骤 3:路由转发
-
鉴权通过后,
chain.filter(exchange)
继续执行。 -
网关根据
Route
中的uri
(http://www.baidu.com
)和predicates
进行转发。 -
最终请求被转发为:
GET http://www.baidu.com/order/16
七、请求调试

网关日志打印日志

下游系统

八、总结
本文从零搭建了一个生产级 API 网关,实现了:
- ✅ 基于路径的动态路由
- ✅ 支持 Body 参与签名的全局鉴权
- ✅ GET/POST 请求兼容处理
- ✅ 高并发性能优化
网关不是简单的"转发器",而是微服务架构的"安全门"与"流量调度中心"。
通过合理设计与优化,即使是 2核4G 的机器,也能扛住上千 QPS,为业务保驾护航。