【架构实战】API网关Spring Cloud Gateway:统一入口的艺术

一、网关选型的血泪史

2019年,公司微服务架构刚刚拆分完成。10个微服务,每个都有自己的Controller,每个都暴露在公网。

噩梦开始:

  • 前端同学要记住10个服务地址
  • 每次服务上线都要通知前端改配置
  • 没有统一的安全策略,各服务各自为战
  • 日志分散,查一个问题要翻10个服务的日志

我开始调研API网关,试过Nginx、Kong、Zuul,最后选择了Spring Cloud Gateway。

今天把踩过的坑和积累的经验分享出来。


二、为什么需要API网关?

2.1 没有网关的痛

复制代码
┌────────────────────────────────────────┐
│              前端应用                     │
└────────────────┬───────────────────────┘
                 │
        ┌────────┼────────┬────────┬───────┐
        ↓        ↓        ↓        ↓       ↓
   ┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
   │用户服务 ││订单服务 ││商品服务 ││支付服务 ││库存服务 │
   │:8081   ││:8082   ││:8083   ││:8084   ││:8085   │
   └────────┘└────────┘└────────┘└────────┘└────────┘

问题:
1. 前端需要知道所有服务地址
2. 无法统一认证和安全
3. 无法统一日志和监控
4. CORS问题严重

2.2 有网关的好处

复制代码
┌────────────────────────────────────────┐
│              前端应用                     │
└────────────────┬───────────────────────┘
                 │
         ┌───────┴───────┐
         │  API Gateway  │
         │   :8080       │
         │               │
         │ - 统一入口     │
         │ - 认证鉴权     │
         │ - 限流熔断     │
         │ - 日志监控     │
         │ - 路由转发     │
         └───────┬───────┘
                 │
        ┌────────┼────────┬────────┬───────┐
        ↓        ↓        ↓        ↓       ↓
   ┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
   │用户服务 ││订单服务 ││商品服务 ││支付服务 ││库存服务 │
   └────────┘└────────┘└────────┘└────────┘└────────┘

好处:
1. 前端只需记住网关地址
2. 统一认证,安全可控
3. 统一日志和监控
4. CORS一次配置

三、Spring Cloud Gateway核心概念

3.1 工作原理

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Spring Cloud Gateway                       │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              Gateway Handler Mapping                   │   │
│  │  - 匹配路由                                            │   │
│  │  - RouteDefinitionLocator                             │   │
│  │  - RoutePredicateHandlerMapping                       │   │
│  └────────────────────┬─────────────────────────────────┘   │
│                       ↓                                       │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                  Gateway Web Handler                   │   │
│  │  - Filter Chain 执行                                   │   │
│  │  - 顺序执行 Pre Filter                                │   │
│  │  - 调用下游服务                                        │   │
│  │  - 顺序执行 Post Filter                               │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

请求流程:
1. 客户端请求到达 Gateway
2. HandlerMapping 匹配路由规则
3. WebHandler 构建 Filter Chain
4. Pre Filter 依次执行(认证、日志、限流...)
5. 调用下游服务
6. Post Filter 逆序执行(响应处理、日志...)
7. 返回客户端

3.2 三大核心概念

java 复制代码
// 1. Route(路由)
// 路由 = 断言 + 过滤器 + 目标URI
Route route = Route.builder()
    .id("order-service")        // 路由ID
    .uri("lb://order-service")  // 目标服务(lb表示负载均衡)
    .predicate(PathRoutePredicateFactory.class, "/order/**")  // 断言
    .filter(FilterA.class)      // 过滤器A
    .filter(FilterB.class)      // 过滤器B
    .build();

// 2. Predicate(断言)
// 用于匹配HTTP请求的各种条件
// - Path:路径匹配
// - Method:请求方法匹配
// - Header:请求头匹配
// - Query:查询参数匹配
// - Cookie:Cookie匹配
// - RemoteAddr:远程地址匹配

// 3. Filter(过滤器)
// 在请求前后做处理
// - Pre Filter:请求转发前执行(认证、日志、限流)
// - Post Filter:响应返回前执行(响应处理、日志)

四、快速入门:第一个Gateway应用

4.1 引入依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>

    <!-- 服务发现支持 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>

4.2 配置文件

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: gateway
  
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    
    gateway:
      # 路由配置
      routes:
        - id: order-service
          uri: lb://order-service  # lb表示负载均衡
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1  # 去掉第一层路径
        
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1
        
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
          filters:
            - StripPrefix=1
      
      # 默认过滤器(对所有路由生效)
      default-filters:
        - DedupeResponseHeader=Vary Access-Control-Allow-Origin

# 日志级别
logging:
  level:
    org.springframework.cloud.gateway: DEBUG

4.3 测试验证

bash 复制代码
# 启动网关后,测试路由转发
curl http://localhost:8080/order/create
# 会被转发到 order-service/create

curl http://localhost:8080/user/info
# 会被转发到 user-service/info

五、路由配置详解

5.1 基础路由配置

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        # 基础路由
        - id: static-route
          uri: http://example.com
          predicates:
            - Path=/static/**
          filters:
            - RewritePath=/static(?<segment>/?.*), $\{segment}

        # 带权重的路由
        - id: order-v1
          uri: http://order-v1.example.com
          predicates:
            - Path=/order/**
            - Weight=80  # 80%流量
        - id: order-v2
          uri: http://order-v2.example.com
          predicates:
            - Path=/order/**
            - Weight=20  # 20%流量

5.2 动态路由(服务发现)

yaml 复制代码
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 开启服务发现路由
          lower-case-service-id: true  # 服务ID小写
          
# 开启后,可以通过以下方式访问:
# http://gateway:8080/order-service/order/create
# 自动转发到 order-service 的 /order/create 接口

# 或者简化路径:
# http://gateway:8080/order-service/order/create
# 如果只配置了 lb://order-service

5.3 路由断言工厂

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: complex-predicates
          uri: lb://user-service
          predicates:
            # 1. Path 路径匹配
            - Path=/user/**
            
            # 2. Method 请求方法
            - Method=GET,POST
            
            # 3. Header 请求头
            - Header=X-Request-Id, \d+
            
            # 4. Query 查询参数
            - Query=name, zhang.*
            
            # 5. Cookie
            - Cookie=session, abc.*
            
            # 6. RemoteAddr 远程地址
            - RemoteAddr=192.168.1.0/24
            
            # 7. After 时间之后
            - After=2024-01-01T00:00:00+08:00[Asia/Shanghai]
            
            # 8. Before 时间之前
            - Before=2024-12-31T23:59:59+08:00[Asia/Shanghai]
            
            # 9. Between 某段时间内
            - Between=2024-01-01T00:00:00+08:00[Asia/Shanghai], \
                     2024-12-31T23:59:59+08:00[Asia/Shanghai]

六、过滤器实战

6.1 内置过滤器

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: with-filters
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            # 1. AddRequestHeader:添加请求头
            - AddRequestHeader=X-Gateway, gateway
            
            # 2. AddRequestHeadersIfNotPresent:添加请求头(不存在时)
            - AddRequestHeadersIfNotPresent=X-Request-Id,123
            
            # 3. RemoveRequestHeader:移除请求头
            - RemoveRequestHeader=X-Internal-Header
            
            # 4. AddResponseHeader:添加响应头
            - AddResponseHeader=X-Gateway, gateway
            
            # 5. RemoveResponseHeader:移除响应头
            - RemoveResponseHeader=Server
            
            # 6. RewritePath:重写路径
            - RewritePath=/order(?<segment>/?.*), $\{segment}
            
            # 7. SetPath:设置路径
            - SetPath=/api${segment}
            
            # 8. StripPrefix:去掉路径前缀
            - StripPrefix=1
            
            # 9. SetStatus:设置状态码
            - SetStatus=401
            
            # 10. RedirectTo:重定向
            - RedirectTo=302, https://example.com

6.2 自定义全局过滤器

java 复制代码
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().value();
        
        // 跳过登录和注册接口
        if (path.startsWith("/auth/login") || path.startsWith("/auth/register")) {
            return chain.filter(exchange);
        }
        
        // 获取Token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        
        if (StringUtils.isBlank(token)) {
            log.warn("请求缺少Token: path={}", path);
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        
        // 去掉Bearer前缀
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        
        // 验证Token
        try {
            Claims claims = jwtUtil.parseToken(token);
            String userId = claims.getSubject();
            
            // 把用户ID放入请求属性
            ServerWebExchange modifiedExchange = exchange.mutate()
                .request(builder -> builder.header("X-User-Id", userId))
                .build();
            
            return chain.filter(modifiedExchange);
            
        } catch (Exception e) {
            log.warn("Token验证失败: {}", e.getMessage());
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
    }
    
    @Override
    public int getOrder() {
        return -100;  // 优先级,数字越小越先执行
    }
}

6.3 自定义路由过滤器

java 复制代码
@Component
@Slf4j
public class RequestLogFilter implements GatewayFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 记录请求开始时间
        long startTime = System.currentTimeMillis();
        String requestId = UUID.randomUUID().toString();
        
        // 打印请求日志
        log.info("请求开始: method={}, path={}, requestId={}",
            request.getMethod(),
            request.getPath(),
            requestId);
        
        // 在响应完成后打印日志
        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                long duration = System.currentTimeMillis() - startTime;
                log.info("请求结束: method={}, path={}, status={}, duration={}ms, requestId={}",
                    request.getMethod(),
                    request.getPath(),
                    exchange.getResponse().getStatusCode(),
                    duration,
                    requestId);
            }));
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

// 在路由中引用
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder, 
                                       RequestLogFilter requestLogFilter) {
    return builder.routes()
        .route("order-service", r -> r
            .path("/order/**")
            .filters(f -> f
                .filter(requestLogFilter)  // 添加自定义过滤器
                .addRequestHeader("X-Gateway", "gateway")
            )
            .uri("lb://order-service"))
        .build();
}

七、限流与熔断

7.1 基于Redis的限流

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: rate-limit-route
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            # 基于Redis的令牌桶限流
            # 100个请求/秒,每个IP最多10个/秒
            - name: RequestRateLimiter
              args:
                redis-token-bucket:
                  capacity: 100          # 令牌桶容量
                  refill-rate: 100       # 每秒补充令牌数
                  key-resolver: "#{@ipKeyResolver}"  # 按IP限流
                redis-token-bucket-per-user:
                  capacity: 10
                  refill-rate: 10
                  key-resolver: "#{@userKeyResolver}"
java 复制代码
@Configuration
public class RateLimitConfig {
    
    /**
     * 按IP限流
     */
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getRemoteAddress() != null
                ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
                : "unknown"
        );
    }
    
    /**
     * 按用户限流
     */
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
            return Mono.just(userId != null ? userId : "anonymous");
        };
    }
    
    /**
     * 按API限流
     */
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getPath().value()
        );
    }
}

7.2 熔断配置

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-spring-cloud-gateway</artifactId>
</dependency>
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: circuit-breaker-route
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - name: CircuitBreaker
              args:
                name: orderCircuitBreaker
                fallbackUri: forward:/fallback/order  # 熔断时的降级URI
java 复制代码
@RestController
@RequestMapping("/fallback")
public class FallbackController {
    
    @GetMapping("/order")
    public Result<?> orderFallback(ServerWebExchange exchange) {
        String path = exchange.getRequest().getPath().value();
        log.warn("Order服务熔断,降级处理: path={}", path);
        return Result.fail(503, "服务暂时不可用,请稍后再试");
    }
    
    @GetMapping("/user")
    public Result<?> userFallback(ServerWebExchange exchange) {
        return Result.fail(503, "用户服务暂时不可用");
    }
}

八、统一认证与鉴权

8.1 JWT认证过滤器

java 复制代码
@Component
@Slf4j
public class JwtAuthFilter implements GlobalFilter {
    
    @Value("${jwt.secret:default-secret}")
    private String secret;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().value();
        
        // 白名单路径跳过认证
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }
        
        String token = getToken(exchange);
        
        if (token == null) {
            return unauthorized(exchange, "未提供认证令牌");
        }
        
        try {
            Claims claims = parseToken(token);
            String userId = claims.getSubject();
            String roles = claims.get("roles", String.class);
            
            // 验证权限
            if (!hasPermission(path, roles)) {
                return forbidden(exchange, "权限不足");
            }
            
            // 添加用户信息到请求头
            ServerWebExchange modified = exchange.mutate()
                .request(builder -> builder
                    .header("X-User-Id", userId)
                    .header("X-User-Roles", roles))
                .build();
            
            return chain.filter(modified);
            
        } catch (Exception e) {
            log.warn("JWT验证失败: {}", e.getMessage());
            return unauthorized(exchange, "令牌无效或已过期");
        }
    }
    
    private boolean isWhiteList(String path) {
        return path.startsWith("/auth/") 
            || path.startsWith("/public/")
            || path.equals("/health");
    }
    
    private boolean hasPermission(String path, String roles) {
        // 管理员可以访问所有接口
        if ("ADMIN".equals(roles)) {
            return true;
        }
        
        // 其他角色按需校验
        if (path.startsWith("/admin/") && !"ADMIN".equals(roles)) {
            return false;
        }
        
        return true;
    }
    
    private String getToken(ServerWebExchange exchange) {
        String auth = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (auth != null && auth.startsWith("Bearer ")) {
            return auth.substring(7);
        }
        return null;
    }
    
    private Claims parseToken(String token) {
        return Jwts.parser()
            .setSigningKey(secret.getBytes())
            .parseClaimsJws(token)
            .getBody();
    }
    
    private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().add("Content-Type", "application/json");
        String body = "{\"code\":401,\"message\":\"" + message + "\"}";
        return exchange.getResponse().writeWith(
            Mono.just(exchange.getResponse().bufferFactory().wrap(body.getBytes()))
        );
    }
    
    private Mono<Void> forbidden(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        exchange.getResponse().getHeaders().add("Content-Type", "application/json");
        String body = "{\"code\":403,\"message\":\"" + message + "\"}";
        return exchange.getResponse().writeWith(
            Mono.just(exchange.getResponse().bufferFactory().wrap(body.getBytes()))
        );
    }
}

8.2 动态权限配置

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        # 公开接口
        - id: public-routes
          uri: lb://public-service
          predicates:
            - Path=/public/**
        
        # 用户接口(需登录)
        - id: user-routes
          uri: lb://user-service
          predicates:
            - Path=/user/**
        
        # 管理员接口(需ADMIN角色)
        - id: admin-routes
          uri: lb://admin-service
          predicates:
            - Path=/admin/**

九、CORS跨域配置

java 复制代码
@Configuration
public class CorsConfig {
    
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);  // 允许携带凭证
        config.addAllowedOriginPattern("*");  // 允许所有来源
        config.addAllowedHeader("*");  // 允许所有请求头
        config.addAllowedMethod("*");  // 允许所有方法
        config.setMaxAge(3600L);  // 预检请求缓存时间
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsWebFilter(source);
    }
}

十、踩坑实录

坑1:路由顺序问题

有一天,一个路由规则不生效。排查半天发现是路由顺序的问题。

原因:Gateway按配置顺序匹配路由,第一个匹配的路由会生效。

解决:把更精确的路由放在前面,更通用的放在后面。

yaml 复制代码
# 错误顺序:/user/detail会先匹配到/user/**
routes:
  - id: user-list
    uri: lb://user-service
    predicates:
      - Path=/user/**
  - id: user-detail  # 永远不会被匹配到
    uri: lb://user-service
    predicates:
      - Path=/user/detail/**

# 正确顺序:
routes:
  - id: user-detail  # 精确路由放前面
    uri: lb://user-service
    predicates:
      - Path=/user/detail/**
  - id: user-list
    uri: lb://user-service
    predicates:
      - Path=/user/**

坑2:Path匹配问题

配置了Path=/order/**,但访问/order//create时报404。

原因//会被认为是路径的一部分。

解决 :在路由过滤器中添加RewritePath,规范化路径。

yaml 复制代码
filters:
  - RewritePath=/order/(?<segment>.*), /$\{segment}

坑3:服务发现路由大小写问题

开启了服务发现路由,但有些服务能访问,有些不能。

原因:服务ID大小写不一致。

解决:开启小写转换。

yaml 复制代码
spring:
  cloud:
    gateway:
      discovery:
        locator:
          lower-case-service-id: true  # 服务ID转小写

坑4:全局过滤器优先级问题

我的认证过滤器明明先执行,但Token还没验证就被路由出去了。

原因:过滤器优先级设置不对。

解决 :使用Ordered接口设置优先级,数字越小越先执行。认证过滤器建议设置为-100-50之间。


十一、总结

Spring Cloud Gateway是微服务架构的统一入口:

  • 路由转发:把请求分发到对应的微服务
  • 断言匹配:灵活匹配HTTP请求的各种条件
  • 过滤器链:在请求前后做统一处理
  • 限流熔断:保护后端服务不被击垮
  • 统一认证:在网关层做认证鉴权

最佳实践:

  1. 路由规则按从精确到粗粒度排序
  2. 认证过滤器优先级要高于其他过滤器
  3. 限流要结合Redis等外部存储
  4. 熔断降级要做好,给用户友好提示
  5. 日志要打全,方便排查问题

血的教训:

网关是所有流量的入口,一旦出问题影响巨大。上线前一定要做好充分的测试,特别是限流和熔断功能。不要以为上了网关就万事大吉,要持续监控网关的各项指标。

思考题: 你的项目用的是什么网关?有没有遇到过路由匹配的问题?


个人观点,仅供参考

相关推荐
前端不太难1 小时前
一个电商鸿蒙 App 的架构设计实战
华为·状态模式·harmonyos
炸炸鱼.1 小时前
从入门到实战:Kubernetes完全指南——概念、架构、集群部署与Dashboard可视化
容器·架构·kubernetes
珠海西格电力10 小时前
零碳园区的能源供给成本主要包括哪些方面?
大数据·分布式·微服务·架构·能源
神奇的程序员10 小时前
重构了自己5年前写的截图插件
前端·javascript·架构
Luhui Dev10 小时前
Anthropic 2026 最新 Agent Harness 架构完整拆解:Managed Agents
人工智能·架构·agent·luhuidev
xifangge202511 小时前
【深度架构】Claude Code + Opus 4.6 全流程调优:API 路由重定向技术与 Agentic 编程流实战(附零成本接入方案)
架构·claudecode·opus4.6
闵孚龙12 小时前
Claude Code API通信层全解析:重试、流式、降级、Fast Mode、Prompt Cache 与 Files API 的底层工程
人工智能·架构·prompt
2601_9577808412 小时前
GPT API工程化接入:从演示验证到生产部署的完整实践
大数据·人工智能·gpt·架构
AirDroid_cn12 小时前
荣耀MagicOS 10系统蜂鸟架构:如何查看应用启动速度提升效果并优化内存回收效率?
架构