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

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

详细请求路径分析

  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 本身 不会主动收到这个请求信息


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

  • 最终请求被转发为:

    复制代码
    GET http://www.baidu.com/order/16

七、请求调试

网关日志打印日志

下游系统

八、总结

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

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

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

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

相关推荐
Mi_Manchikkk10 小时前
Java高级面试实战:Spring Boot微服务与Redis缓存整合案例解析
java·spring boot·redis·缓存·微服务·面试
熊出没10 小时前
微服务如何集成swagger3
微服务·云原生·架构
LKAI.1 天前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
天上掉下来个程小白1 天前
微服务-02.认识微服务-单体架构
微服务·云原生·架构
nshkfhwr1 天前
什么是微服务
微服务·云原生·架构·云计算·集群
2301_793086871 天前
SpringCloud 07 微服务网关
java·spring cloud·微服务
forestsea1 天前
微服务远程调用完全透传实现:响应式与非响应式解决方案
微服务·云原生·架构
励志成为糕手1 天前
企业级Spring事务管理:从单体应用到微服务分布式事务完整方案
分布式·spring·微服务·隔离级别·事务管理
天上掉下来个程小白1 天前
微服务-01.导入黑马商城
java·微服务·架构