🌐 从零构建高可用 API 网关:鉴权、路由、性能优化全解析
从零构建高可用 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,性能高、扩展性强,是微服务网关的理想选择。
四、架构设计:整体架构与模块划分
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
@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本身 不会主动收到这个请求信息。
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,为业务保驾护航。
🌐 从零构建高可用 API 网关:鉴权、路由、性能优化全解析的更多相关文章
- 高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群
高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群 libnet软件包<-依赖-heartbeat(包含ldirectord插件(需要perl-MailTools的rpm包)) l ...
- 基于docker+etcd+confd + haproxy构建高可用、自发现的web服务
基于docker+etcd+confd + haproxy构建高可用.自发现的web服务 2016-05-16 15:12 595人阅读 评论(0) 收藏 举报 版权声明:本文为博主原创文章,未经博主 ...
- 用HAProxy和KeepAlived构建高可用的反向代理
用HAProxy和KeepAlived构建高可用的反向代理 用HAProxy和KeepAlived构建高可用的反向代理 前言对于访问量较大的网站来说,随着流量的增加单台服务器已经无法处理所有的请求 ...
- (转)Ubuntu 12.04 LTS 构建高可用分布式 MySQL 集群
本文的英文版本链接是 http://www.mrxuri.com/index.php/2013/11/20/install-mysql-cluster-on-ubuntu-12-04-lts.html ...
- Ubuntu 12.04 LTS 构建高可用分布式 MySQL 集群
本文的英文版本链接是 http://xuri.me/2013/11/20/install-mysql-cluster-on-ubuntu-12-04-lts.html MySQL Cluster 是 ...
- .net core下简单构建高可用服务集群
一说到集群服务相信对普通开发者来说肯定想到很复杂的事情,如zeekeeper ,反向代理服务网关等一系列的搭建和配置等等:总得来说需要有一定经验和规划的团队才能应用起来.在这文章里你能看到在.net ...
- 高可用api接口网络部署方案
我们平时接触的产品都是7*24小时不间断服务,产品中的api接口肯定也是高可用的,下面我向大家分享一下互联网公司api接口高可用的网络部署方案. 我们一般通过http://le.quwenzhe.c ...
- 用HAProxy和KeepAlived构建高可用的反向代理系统
对于访问量较大的网站来说,随着流量的增加单台服务器已经无法处理所有的请求,这时候需要多台服务器对大量的请求进行分流处理,即负载均衡.而如果实现负载均衡,必须在网站的入口部署服务器(不只是一台)对这些请 ...
- 每天响应数亿次请求,腾讯云如何提供高可用API服务?
每天响应数亿次请求,腾讯云如何提供高可用API服务? https://mp.weixin.qq.com/s/OPwlHcqkaTT_gcwHfr5Shw 李阳 云加社区 2020-09-16 导语 | ...
- Linux企业集群用商用硬件和免费软件构建高可用集群PDF
Linux企业集群:用商用硬件和免费软件构建高可用集群 目录: 译者序致谢前言绪论第一部分 集群资源 第1章 启动服务 第2章 处理数据包 第3章 编译内容 第二部分 高可用性 第4章 使用rsync ...
随机推荐
- ImportError: lxml.html.clean module is now a separate project lxml_html_clean
导包报错 from lxml_html_clean import Cleaner 解决报错:"ImportError: lxml.html.clean module is now a sep ...
- 运维人员常用Linux命令汇总
作为运维人员,这些常用命令不得不会,掌握这些命令,工作上会事半功倍,提供工作效率. 一.文件和目录 cd命令,用于切换当前目录,它的参数是要切换到的目录的路径,可以是绝对路径,也可以是相对路径. cd ...
- slf4j、logback、log4j、log4j2的区别
区别 slf4j是一个日志接口,自己没有具体实现日志系统,只提供了一组标准的调用api,这样将调用和具体的日志实现分离,使用slf4j后有利于根据自己实际的需求更换具体的日志系统,比如,之前使用的具体 ...
- 总决赛定档!“天翼云息壤杯”高校AI大赛巅峰之战即将打响!
近日,为梦想添翼,让AI发光--"天翼云息壤杯"高校AI大赛总决赛时间正式揭晓.总决赛将于2025年7月1日至7月17日在北京举办.届时,来自全国各地上百支成功晋级的优秀队伍和特邀 ...
- FFmpeg开发笔记(六十三)FFmpeg使用vvenc把视频转为H.266编码
前面的两篇文章分别介绍了如何在Linux环境和Windows环境给FFmpeg集成H.266的编码器vvenc,接下来利用ffmpeg把视频文件转换为VVC格式,观察新生成的vvc视频能否正常播放. ...
- Spring AI 玩转多轮对话
AI "失忆"怎么办?本文带你用 Spring AI 一招搞定多轮对话,让你的 AI 应用拥有超强记忆!从 ChatClient.Advisors 到实战编码,三步打造一个能记住上 ...
- 学习spring cloud记录7-nacos服务分级存储模型
前言 添加集群,级别分别为服务--集群--实例. 配置集群 可在配置文件中添加以下配置设置该服务的集群 cloud: nacos: server-addr: localhost:8848 # naco ...
- ORACLE--SQL日常问题和技巧2(自定义排序,递归查询,异常ORA-01747,逗号隔开的字符串转成in条件,用符号连接表中某字段)
1.有些情况需要将几条记录按要求排序,适用于少量要求 表如图所示: 按照e,u,r,o,t,w,q,y,i顺序排序: 1 SELECT 2 * 3 FROM 4 LGQ_TEST 5 ORDER BY ...
- git冲突的解决与版本回退遇到的问题
这里有2个分支,master中的test.txt文件内容是master init,branch01中的test.txt文件内容是branch01 init 1.合并分支遇到冲突 在master上 ...
- 微服务架构PaaS平台,iPaaS平台支撑底座
RestCloud所有产品均基于本微服务架构PaaS平台研发而来,底层PaaS平台是RestCloud所有产品的技术底座,基于本技术底座RestCloud快速研发了所有产品线,通过不断迭代PaaS平台 ...