Skip to content

API网关

基础概念

什么是API网关?

答案: API网关是微服务架构中的统一入口,负责请求路由、协议转换、安全认证、流量控制等功能。

核心功能

  • 路由转发:将请求路由到对应的微服务
  • 负载均衡:分发请求到多个服务实例
  • 认证鉴权:统一的身份验证和权限控制
  • 限流熔断:保护后端服务
  • 协议转换:HTTP、gRPC、WebSocket等协议转换
  • 日志监控:统一的日志记录和监控
  • 缓存:减少后端服务压力

为什么需要API网关?

答案:

没有网关的问题

  • 客户端需要维护多个服务地址
  • 认证鉴权逻辑分散在各个服务
  • 跨域、限流等通用逻辑重复实现
  • 协议不统一

使用网关的优势

  • 统一入口,简化客户端调用
  • 集中处理横切关注点
  • 服务解耦,后端服务对客户端透明
  • 便于监控和管理

架构对比

# 没有网关
客户端 → 服务A
客户端 → 服务B
客户端 → 服务C

# 使用网关
客户端 → API网关 → 服务A

            服务B

            服务C

常见的API网关?

答案:

网关特点适用场景
Spring Cloud Gateway基于Spring 5、WebFlux,异步非阻塞Spring Cloud生态
ZuulNetflix开源,同步阻塞传统Spring Cloud
Kong基于Nginx+Lua,高性能大流量场景
Nginx高性能,配置简单简单路由场景
Traefik云原生,支持多种协议Kubernetes环境

Spring Cloud Gateway

Gateway的核心概念?

答案:

三大核心

1. Route(路由)

  • 网关的基本构建块
  • 包含ID、目标URI、断言集合、过滤器集合

2. Predicate(断言)

  • 匹配HTTP请求的条件
  • 如路径、方法、请求头等

3. Filter(过滤器)

  • 修改请求和响应
  • 分为Pre和Post两种

工作流程

客户端请求

Gateway Handler Mapping(匹配路由)

Predicate(断言判断)

Gateway Web Handler

Pre Filter(前置过滤)

代理请求到目标服务

Post Filter(后置过滤)

返回响应

Gateway的路由配置?

答案:

方式1:配置文件

yaml
spring:
  cloud:
    gateway:
      routes:
        # 路由1:用户服务
        - id: user-service
          uri: lb://user-service  # lb表示负载均衡
          predicates:
            - Path=/user/**
            - Method=GET,POST
            - Header=X-Request-Id, \d+
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Request-Source, Gateway

        # 路由2:订单服务
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
            - After=2024-01-01T00:00:00+08:00[Asia/Shanghai]
          filters:
            - RewritePath=/order/(?<segment>.*), /${segment}

        # 路由3:静态资源
        - id: static-resource
          uri: http://cdn.example.com
          predicates:
            - Path=/static/**

方式2:Java配置

java
@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r
                .path("/user/**")
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Request-Source", "Gateway")
                )
                .uri("lb://user-service")
            )
            .route("order-service", r -> r
                .path("/order/**")
                .filters(f -> f
                    .rewritePath("/order/(?<segment>.*)", "/${segment}")
                )
                .uri("lb://order-service")
            )
            .build();
    }
}

Gateway的断言工厂?

答案:

内置断言工厂

1. 路径断言

yaml
predicates:
  - Path=/user/{id}  # 路径匹配
  - Path=/api/**  # 通配符

2. 方法断言

yaml
predicates:
  - Method=GET,POST

3. 请求头断言

yaml
predicates:
  - Header=X-Request-Id, \d+  # 正则匹配
  - Header=Authorization

4. 请求参数断言

yaml
predicates:
  - Query=token  # 必须有token参数
  - Query=version, 1.*  # version参数匹配1.*

5. Cookie断言

yaml
predicates:
  - Cookie=sessionId, [a-z0-9]+

6. 时间断言

yaml
predicates:
  - After=2024-01-01T00:00:00+08:00[Asia/Shanghai]
  - Before=2024-12-31T23:59:59+08:00[Asia/Shanghai]
  - Between=2024-01-01T00:00:00+08:00[Asia/Shanghai],2024-12-31T23:59:59+08:00[Asia/Shanghai]

7. 权重断言

yaml
# 80%流量到v1,20%流量到v2
- id: service-v1
  uri: lb://service-v1
  predicates:
    - Weight=group1, 8

- id: service-v2
  uri: lb://service-v2
  predicates:
    - Weight=group1, 2

自定义断言

java
@Component
public class CustomRoutePredicateFactory
    extends AbstractRoutePredicateFactory<CustomRoutePredicateFactory.Config> {

    public CustomRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("userId");
            return config.getUserIds().contains(userId);
        };
    }

    public static class Config {
        private List<String> userIds;
        // getter/setter
    }
}

Gateway的过滤器?

答案:

内置过滤器

1. 请求头过滤器

yaml
filters:
  - AddRequestHeader=X-Request-Id, 123
  - RemoveRequestHeader=Cookie
  - SetRequestHeader=X-Request-Source, Gateway

2. 响应头过滤器

yaml
filters:
  - AddResponseHeader=X-Response-Time, 100ms
  - RemoveResponseHeader=Set-Cookie
  - SetResponseHeader=X-Response-Source, Gateway

3. 路径过滤器

yaml
filters:
  - StripPrefix=1  # 去掉路径第一层
  - PrefixPath=/api  # 添加路径前缀
  - RewritePath=/old/(?<segment>.*), /new/${segment}

4. 重定向过滤器

yaml
filters:
  - RedirectTo=302, https://www.example.com

5. 重试过滤器

yaml
filters:
  - name: Retry
    args:
      retries: 3
      statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
      methods: GET,POST
      backoff:
        firstBackoff: 10ms
        maxBackoff: 50ms
        factor: 2

6. 限流过滤器

yaml
filters:
  - name: RequestRateLimiter
    args:
      redis-rate-limiter.replenishRate: 10  # 令牌桶填充速率
      redis-rate-limiter.burstCapacity: 20  # 令牌桶容量
      key-resolver: "#{@userKeyResolver}"
java
@Bean
public KeyResolver userKeyResolver() {
    return exchange -> Mono.just(
        exchange.getRequest().getHeaders().getFirst("userId")
    );
}

7. 熔断过滤器

yaml
filters:
  - name: CircuitBreaker
    args:
      name: myCircuitBreaker
      fallbackUri: forward:/fallback
java
@RestController
public class FallbackController {

    @GetMapping("/fallback")
    public Result fallback() {
        return Result.error("服务暂时不可用");
    }
}

认证鉴权

如何实现统一认证?

答案:

方案1:JWT认证

java
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        // 白名单
        if (isWhitelist(request.getPath().value())) {
            return chain.filter(exchange);
        }

        // 获取token
        String token = request.getHeaders().getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return unauthorized(exchange);
        }

        token = token.substring(7);

        // 验证token
        try {
            Claims claims = jwtUtil.parseToken(token);
            String userId = claims.getSubject();

            // 将用户信息传递给下游服务
            ServerHttpRequest newRequest = request.mutate()
                .header("X-User-Id", userId)
                .build();

            return chain.filter(exchange.mutate().request(newRequest).build());

        } catch (Exception e) {
            return unauthorized(exchange);
        }
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

方案2:OAuth2认证

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/auth
java
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .pathMatchers("/public/**").permitAll()
                .anyExchange().authenticated()
            .and()
            .oauth2ResourceServer()
                .jwt();

        return http.build();
    }
}

如何实现权限控制?

答案:

基于角色的权限控制(RBAC)

java
@Component
public class AuthorizationFilter implements GlobalFilter, Ordered {

    @Autowired
    private PermissionService permissionService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
        String path = exchange.getRequest().getPath().value();
        String method = exchange.getRequest().getMethod().name();

        // 检查权限
        if (!permissionService.hasPermission(userId, path, method)) {
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -90;
    }
}
java
@Service
public class PermissionService {

    @Autowired
    private RedisTemplate<String, Set<String>> redisTemplate;

    public boolean hasPermission(String userId, String path, String method) {
        // 从Redis获取用户权限
        Set<String> permissions = redisTemplate.opsForValue().get("user:permission:" + userId);

        if (permissions == null) {
            return false;
        }

        // 检查是否有权限
        String permission = method + ":" + path;
        return permissions.contains(permission) || permissions.contains("*:*");
    }
}

限流

如何实现网关限流?

答案:

方案1:基于Redis的令牌桶限流

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10  # 每秒生成10个令牌
                redis-rate-limiter.burstCapacity: 20  # 令牌桶容量20
                redis-rate-limiter.requestedTokens: 1  # 每次请求消耗1个令牌
                key-resolver: "#{@ipKeyResolver}"
java
@Configuration
public class RateLimiterConfig {

    // 基于IP限流
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
        );
    }

    // 基于用户限流
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getHeaders().getFirst("userId")
        );
    }

    // 基于接口限流
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getPath().value()
        );
    }
}

方案2:自定义限流过滤器

java
@Component
public class CustomRateLimiterFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String key = getKey(exchange);

        // 使用Lua脚本实现令牌桶
        String luaScript =
            "local key = KEYS[1]\n" +
            "local capacity = tonumber(ARGV[1])\n" +
            "local rate = tonumber(ARGV[2])\n" +
            "local now = tonumber(ARGV[3])\n" +
            "...\n";  // 完整的Lua脚本

        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            "20", "10", String.valueOf(System.currentTimeMillis())
        );

        if (result == null || result == 0) {
            // 限流
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    private String getKey(ServerWebExchange exchange) {
        String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
        String path = exchange.getRequest().getPath().value();
        return "rate_limiter:" + ip + ":" + path;
    }

    @Override
    public int getOrder() {
        return -80;
    }
}

缓存

如何实现网关缓存?

答案:

java
@Component
public class CacheFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        // 只缓存GET请求
        if (!HttpMethod.GET.equals(request.getMethod())) {
            return chain.filter(exchange);
        }

        String cacheKey = getCacheKey(request);

        // 查询缓存
        String cachedResponse = redisTemplate.opsForValue().get(cacheKey);
        if (cachedResponse != null) {
            // 返回缓存数据
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            DataBuffer buffer = response.bufferFactory().wrap(cachedResponse.getBytes());
            return response.writeWith(Mono.just(buffer));
        }

        // 缓存响应
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse()) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                        DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                        DataBuffer join = dataBufferFactory.join(dataBuffers);
                        byte[] content = new byte[join.readableByteCount()];
                        join.read(content);
                        DataBufferUtils.release(join);

                        // 缓存响应
                        String responseBody = new String(content, StandardCharsets.UTF_8);
                        redisTemplate.opsForValue().set(cacheKey, responseBody, 60, TimeUnit.SECONDS);

                        return dataBufferFactory.wrap(content);
                    }));
                }
                return super.writeWith(body);
            }
        };

        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    private String getCacheKey(ServerHttpRequest request) {
        return "cache:" + request.getPath().value() + ":" + request.getQueryParams().toString();
    }

    @Override
    public int getOrder() {
        return -70;
    }
}

日志监控

如何实现请求日志记录?

答案:

java
@Component
@Slf4j
public class LogFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String requestId = UUID.randomUUID().toString();

        // 记录请求日志
        log.info("请求开始 - requestId: {}, method: {}, path: {}, params: {}",
            requestId,
            request.getMethod(),
            request.getPath(),
            request.getQueryParams()
        );

        long startTime = System.currentTimeMillis();

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            long duration = System.currentTimeMillis() - startTime;

            // 记录响应日志
            log.info("请求结束 - requestId: {}, status: {}, duration: {}ms",
                requestId,
                response.getStatusCode(),
                duration
            );
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

练习题

  1. 如何实现网关的高可用?
  2. 如何处理网关的跨域问题?
  3. 如何实现动态路由配置?
  4. 网关如何实现灰度发布?
  5. 如何优化网关的性能?

Released under the MIT License.