🌐 从零构建高可用 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,性能高、扩展性强,是微服务网关的理想选择。


四、架构设计:整体架构与模块划分

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 为例:

&#127760; 从零构建高可用 API 网关:鉴权、路由、性能优化全解析

详细请求路径分析

  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 

七、请求调试

&#127760; 从零构建高可用 API 网关:鉴权、路由、性能优化全解析

网关日志打印日志

&#127760; 从零构建高可用 API 网关:鉴权、路由、性能优化全解析

下游系统

&#127760; 从零构建高可用 API 网关:鉴权、路由、性能优化全解析

八、总结

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

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

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

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

发表评论

评论已关闭。

相关文章