从零构建高可用 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]

核心模块

  1. 动态路由模块:基于 RouteLocator 实现路径匹配
  2. 全局过滤器GlobalFilter 实现鉴权、日志等
  3. 安全模块:自定义签名验证(HMAC-SHA256)
  4. 性能优化: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 为例:

详细请求路径分析

  1. 请求进入网关

  2. RoutePredicateHandlerMapping:匹配路由

  3. FilteringWebHandler:执行 GlobalFilter 和 GatewayFilter

  4. 路由转发到目标地址(如 http://www.baidu.com/order/16)

我们一步步来看:

步骤 1:路由匹配(Route Matching)

  • 网关会从所有可用的 RouteDefinition 中查找匹配的路由。

  • 你的两个路由源都会被加载:

  • 来自 CustomRouteConfigPath=/pass/order/**http://www.baidu.com

    • 来自 application.ymlPath=/pass/**http://localhost:8080
  • 由于 /pass/order/** 是更具体的路径,它会优先匹配(前提是 order 设置合理)。

  • 匹配成功后,生成一个Route对象,包含:

  ID: `order_route`
URI: `http://www.baidu.com`
Predicates: `Path=/pass/order/**`
Filters: (如果有)

步骤 2:执行 GlobalFilter(AuthGlobalFilter)

  • 匹配路由后,网关进入过滤器链。

  • AuthGlobalFilterGlobalFilter,它的 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 中的 urihttp://www.baidu.com)和 predicates 进行转发。

  • 最终请求被转发为:

    GET http://www.baidu.com/order/16

七、请求调试

网关日志打印日志

下游系统

八、总结

本文从零搭建了一个生产级 API 网关,实现了:

  • 基于路径的动态路由
  • 支持 Body 参与签名的全局鉴权
  • GET/POST 请求兼容处理
  • 高并发性能优化

网关不是简单的“转发器”,而是微服务架构的“安全门”与“流量调度中心”

通过合理设计与优化,即使是 2核4G 的机器,也能扛住上千 QPS,为业务保驾护航。

🌐 从零构建高可用 API 网关:鉴权、路由、性能优化全解析的更多相关文章

  1. 高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群

    高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群 libnet软件包<-依赖-heartbeat(包含ldirectord插件(需要perl-MailTools的rpm包)) l ...

  2. 基于docker+etcd+confd + haproxy构建高可用、自发现的web服务

    基于docker+etcd+confd + haproxy构建高可用.自发现的web服务 2016-05-16 15:12 595人阅读 评论(0) 收藏 举报 版权声明:本文为博主原创文章,未经博主 ...

  3. 用HAProxy和KeepAlived构建高可用的反向代理

      用HAProxy和KeepAlived构建高可用的反向代理 用HAProxy和KeepAlived构建高可用的反向代理 前言对于访问量较大的网站来说,随着流量的增加单台服务器已经无法处理所有的请求 ...

  4. (转)Ubuntu 12.04 LTS 构建高可用分布式 MySQL 集群

    本文的英文版本链接是 http://www.mrxuri.com/index.php/2013/11/20/install-mysql-cluster-on-ubuntu-12-04-lts.html ...

  5. Ubuntu 12.04 LTS 构建高可用分布式 MySQL 集群

    本文的英文版本链接是 http://xuri.me/2013/11/20/install-mysql-cluster-on-ubuntu-12-04-lts.html MySQL Cluster 是 ...

  6. .net core下简单构建高可用服务集群

    一说到集群服务相信对普通开发者来说肯定想到很复杂的事情,如zeekeeper ,反向代理服务网关等一系列的搭建和配置等等:总得来说需要有一定经验和规划的团队才能应用起来.在这文章里你能看到在.net ...

  7. 高可用api接口网络部署方案

    我们平时接触的产品都是7*24小时不间断服务,产品中的api接口肯定也是高可用的,下面我向大家分享一下互联网公司api接口高可用的网络部署方案.  我们一般通过http://le.quwenzhe.c ...

  8. 用HAProxy和KeepAlived构建高可用的反向代理系统

    对于访问量较大的网站来说,随着流量的增加单台服务器已经无法处理所有的请求,这时候需要多台服务器对大量的请求进行分流处理,即负载均衡.而如果实现负载均衡,必须在网站的入口部署服务器(不只是一台)对这些请 ...

  9. 每天响应数亿次请求,腾讯云如何提供高可用API服务?

    每天响应数亿次请求,腾讯云如何提供高可用API服务? https://mp.weixin.qq.com/s/OPwlHcqkaTT_gcwHfr5Shw 李阳 云加社区 2020-09-16 导语 | ...

  10. Linux企业集群用商用硬件和免费软件构建高可用集群PDF

    Linux企业集群:用商用硬件和免费软件构建高可用集群 目录: 译者序致谢前言绪论第一部分 集群资源 第1章 启动服务 第2章 处理数据包 第3章 编译内容 第二部分 高可用性 第4章 使用rsync ...

随机推荐

  1. 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 ...

  2. 运维人员常用Linux命令汇总

    作为运维人员,这些常用命令不得不会,掌握这些命令,工作上会事半功倍,提供工作效率. 一.文件和目录 cd命令,用于切换当前目录,它的参数是要切换到的目录的路径,可以是绝对路径,也可以是相对路径. cd ...

  3. slf4j、logback、log4j、log4j2的区别

    区别 slf4j是一个日志接口,自己没有具体实现日志系统,只提供了一组标准的调用api,这样将调用和具体的日志实现分离,使用slf4j后有利于根据自己实际的需求更换具体的日志系统,比如,之前使用的具体 ...

  4. 总决赛定档!“天翼云息壤杯”高校AI大赛巅峰之战即将打响!

    近日,为梦想添翼,让AI发光--"天翼云息壤杯"高校AI大赛总决赛时间正式揭晓.总决赛将于2025年7月1日至7月17日在北京举办.届时,来自全国各地上百支成功晋级的优秀队伍和特邀 ...

  5. FFmpeg开发笔记(六十三)FFmpeg使用vvenc把视频转为H.266编码

    ​前面的两篇文章分别介绍了如何在Linux环境和Windows环境给FFmpeg集成H.266的编码器vvenc,接下来利用ffmpeg把视频文件转换为VVC格式,观察新生成的vvc视频能否正常播放. ...

  6. Spring AI 玩转多轮对话

    AI "失忆"怎么办?本文带你用 Spring AI 一招搞定多轮对话,让你的 AI 应用拥有超强记忆!从 ChatClient.Advisors 到实战编码,三步打造一个能记住上 ...

  7. 学习spring cloud记录7-nacos服务分级存储模型

    前言 添加集群,级别分别为服务--集群--实例. 配置集群 可在配置文件中添加以下配置设置该服务的集群 cloud: nacos: server-addr: localhost:8848 # naco ...

  8. 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 ...

  9. git冲突的解决与版本回退遇到的问题

    这里有2个分支,master中的test.txt文件内容是master init,branch01中的test.txt文件内容是branch01 init    1.合并分支遇到冲突 在master上 ...

  10. 微服务架构PaaS平台,iPaaS平台支撑底座

    RestCloud所有产品均基于本微服务架构PaaS平台研发而来,底层PaaS平台是RestCloud所有产品的技术底座,基于本技术底座RestCloud快速研发了所有产品线,通过不断迭代PaaS平台 ...