Java `API Gateway` 设计 (`Spring Cloud Gateway`, `Zuul`) `Authentication`, `Rate Limiting`, `Routing`

各位靓仔靓女,今天咱们来聊聊Java API Gateway的设计,主要围绕Spring Cloud GatewayZuul,重点攻克Authentication(认证)、Rate Limiting(限流)和Routing(路由)这三大难题。准备好了吗?开始发车!

一、API Gateway:站在门口的大管家

想象一下,你的后宫(微服务集群)佳丽三千,每个妃子(微服务)都有自己的专长。皇帝(前端应用)想要宠幸哪个妃子,总不能直接冲进后宫乱来吧?这时候,就需要一个大管家(API Gateway),负责:

  • 验明正身 (Authentication): 确认皇帝是不是真的皇帝,有没有资格宠幸后宫。
  • 雨露均沾 (Rate Limiting): 防止皇帝短时间内把某个妃子榨干,保证后宫和谐稳定。
  • 指路明灯 (Routing): 引导皇帝准确找到想宠幸的妃子,避免走错房间。

所以,API Gateway的核心作用就是:把外部请求统一入口,进行身份验证、流量控制和路由转发,最终将请求导向后端微服务。

二、两大门神:Spring Cloud Gateway vs. Zuul

目前Java界比较流行的两大API Gateway方案就是Spring Cloud GatewayZuul

特性 Spring Cloud Gateway Zuul (Netflix Zuul 1.x / 2.x)
底层架构 基于Spring WebFlux,采用非阻塞、事件驱动的Reactor模式。 Zuul 1.x 基于Servlet,采用阻塞式I/O。Zuul 2.x 基于Netty,支持异步非阻塞I/O。
性能 理论上性能更高,尤其是在高并发场景下,更能体现优势。 Zuul 1.x 性能相对较低,在高并发场景下容易出现瓶颈。Zuul 2.x 性能有所提升,但仍需根据实际情况进行测试和优化。
Spring集成 与Spring Cloud生态系统集成度更高,配置更加方便,易于使用Spring Boot的各种特性。 与Spring Cloud的集成相对较弱,配置相对复杂。
编程模型 使用Route Locator Builder和GatewayFilter来实现路由和过滤,配置方式更加灵活和强大。 使用过滤器(Filter)来实现路由和过滤,配置方式相对简单。
动态路由 支持动态路由,可以从配置中心(如Nacos、Consul)动态更新路由规则。 支持动态路由,但需要自定义实现,相对复杂。
维护状态 Spring Cloud Gateway 正在积极维护和更新。 Netflix Zuul 1.x 已经停止维护。Zuul 2.x 的活跃度相对较低。
推荐程度 如果你追求高性能、更好的Spring集成和更灵活的配置方式,并且对响应式编程有一定了解,那么Spring Cloud Gateway是更好的选择。 如果你的项目已经使用了Zuul 1.x,并且不需要太高的性能,可以继续使用。但如果需要更高的性能和更好的可维护性,建议迁移到Spring Cloud Gateway或Zuul 2.x。

简单来说,Spring Cloud Gateway是后起之秀,基于更先进的技术,性能更好,与Spring Cloud生态集成更紧密。Zuul是老牌劲旅,但Zuul 1.x已经逐渐淡出舞台。

三、Authentication:验明正身,才能入宫

API Gateway的第一道防线就是Authentication。常用的认证方式有:

  • Basic Authentication: 简单粗暴,直接在请求头里放用户名和密码。不安全,不推荐。
  • Session Authentication: 用户登录后,服务器生成一个Session ID,保存在Cookie里。依赖Session共享,在分布式环境下比较麻烦。
  • Token Authentication (JWT): 用户登录后,服务器生成一个Token,每次请求都带上Token。无状态,易于扩展,推荐使用。
  • OAuth 2.0: 授权框架,用户授权第三方应用访问自己的资源。适用于开放平台。

这里我们重点讲讲JWT (JSON Web Token) 的实现。

JWT原理:

JWT是一个JSON对象,包含了三个部分:

  1. Header (头部): 声明Token类型和加密算法。
  2. Payload (载荷): 存放用户信息和声明 (Claims)。
  3. Signature (签名): 对Header和Payload进行加密,防止篡改。

JWT认证流程:

  1. 用户提供用户名和密码登录。
  2. 服务器验证用户信息,如果正确,生成JWT。
  3. 服务器将JWT返回给客户端。
  4. 客户端将JWT保存在本地 (例如LocalStorage或Cookie)。
  5. 客户端每次请求API时,都在请求头里带上JWT (通常放在Authorization头部,格式为:Authorization: Bearer <JWT> )。
  6. API Gateway接收到请求,验证JWT的有效性。
  7. 如果JWT有效,则放行请求;否则,拒绝请求。

Spring Cloud Gateway + JWT实现:

  1. 引入依赖:

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
  2. JWT工具类:

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.Map;
    
    @Component
    public class JwtUtil {
    
        @Value("${jwt.secret}")
        private String secret;
    
        @Value("${jwt.expiration}")
        private long expiration;
    
        // 生成JWT
        public String generateToken(Map<String, Object> claims) {
            Date now = new Date();
            Date expiryDate = new Date(now.getTime() + expiration * 1000);
    
            return Jwts.builder()
                    .setClaims(claims)
                    .setIssuedAt(now)
                    .setExpiration(expiryDate)
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }
    
        // 从JWT中获取Claims
        public Claims getClaimsFromToken(String token) {
            try {
                return Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                return null; // Token无效
            }
        }
    
        // 验证Token是否有效
        public boolean validateToken(String token) {
            Claims claims = getClaimsFromToken(token);
            return claims != null && claims.getExpiration().after(new Date());
        }
    
        // 从Token中获取用户名
        public String getUsernameFromToken(String token) {
            Claims claims = getClaimsFromToken(token);
            return claims != null ? claims.getSubject() : null;
        }
    }
    • jwt.secretjwt.expiration 需要在application.yml中配置。
  3. 自定义GatewayFilter:

    import org.springframework.cloud.gateway.filter.GatewayFilter;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    @Component
    public class AuthenticationFilter implements GatewayFilter {
    
        private final JwtUtil jwtUtil;
    
        public AuthenticationFilter(JwtUtil jwtUtil) {
            this.jwtUtil = jwtUtil;
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            String authHeader = request.getHeaders().getFirst("Authorization");
    
            if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
    
            String token = authHeader.substring(7); // 去掉 "Bearer " 前缀
    
            if (!jwtUtil.validateToken(token)) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
    
            // 将用户信息放入请求头,方便后续微服务使用
            String username = jwtUtil.getUsernameFromToken(token);
            ServerHttpRequest modifiedRequest = request.mutate()
                    .header("X-User-Name", username)
                    .build();
    
            return chain.filter(exchange.mutate().request(modifiedRequest).build());
        }
    }
  4. 配置路由:

    import org.springframework.cloud.gateway.route.RouteLocator;
    import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class GatewayConfig {
    
        private final AuthenticationFilter authenticationFilter;
    
        public GatewayConfig(AuthenticationFilter authenticationFilter) {
            this.authenticationFilter = authenticationFilter;
        }
    
        @Bean
        public RouteLocator routes(RouteLocatorBuilder builder) {
            return builder.routes()
                    // 需要认证的路由
                    .route("auth_route", r -> r.path("/api/**") // 匹配 /api/** 的请求
                            .filters(f -> f.filter(authenticationFilter)) // 应用 AuthenticationFilter
                            .uri("lb://your-service")) // 转发到名为 your-service 的微服务 (使用LoadBalancerClient)
                    // 不需要认证的路由
                    .route("public_route", r -> r.path("/public/**") // 匹配 /public/** 的请求
                            .uri("lb://your-service")) // 转发到名为 your-service 的微服务
                    .build();
        }
    }

Zuul + JWT实现 (以Zuul 1.x为例):

  1. 引入依赖: 同Spring Cloud Gateway

  2. JWT工具类: 同Spring Cloud Gateway

  3. 自定义Zuul Filter:

    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    
    @Component
    public class AuthenticationFilter extends ZuulFilter {
    
        @Autowired
        private JwtUtil jwtUtil;
    
        @Override
        public String filterType() {
            return "pre"; // 在请求路由之前执行
        }
    
        @Override
        public int filterOrder() {
            return 1; // 优先级,数字越小优先级越高
        }
    
        @Override
        public boolean shouldFilter() {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
    
            // 可以根据请求路径判断是否需要过滤
            String uri = request.getRequestURI();
            return uri.startsWith("/api/"); // 只过滤 /api/ 开头的请求
        }
    
        @Override
        public Object run() {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String authHeader = request.getHeader("Authorization");
    
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                ctx.setSendZuulResponse(false); // 阻止路由
                ctx.setResponseStatusCode(401); // 返回401状态码
                return null;
            }
    
            String token = authHeader.substring(7);
    
            if (!jwtUtil.validateToken(token)) {
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(401);
                return null;
            }
    
            // 将用户信息放入请求头,方便后续微服务使用
            String username = jwtUtil.getUsernameFromToken(token);
            ctx.addZuulRequestHeader("X-User-Name", username);
    
            return null;
        }
    }
  4. 配置路由 (application.yml):

    zuul:
      routes:
        your-service: # 路由名称
          path: /api/** # 匹配 /api/** 的请求
          url: http://your-service:8080 # 转发到的微服务地址 (不推荐直接写死,建议使用服务发现)
          # serviceId: your-service # 使用服务发现,转发到名为 your-service 的微服务

四、Rate Limiting:雨露均沾,后宫才能和谐

API Gateway的第二道防线就是Rate Limiting,防止恶意请求或流量突增导致后端服务崩溃。常用的限流算法有:

  • 令牌桶 (Token Bucket): 以恒定速率向桶中放入令牌,每个请求消耗一个令牌。如果桶中没有令牌,则拒绝请求。
  • 漏桶 (Leaky Bucket): 请求进入漏桶,以恒定速率从漏桶中漏出。如果漏桶满了,则拒绝请求。
  • 计数器 (Counter): 在一定时间内,记录请求次数。如果超过阈值,则拒绝请求。

这里我们重点讲讲令牌桶算法的实现。

Spring Cloud Gateway + Redis + Lua脚本实现令牌桶:

  1. 引入依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
  2. RateLimiter配置:

    import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import reactor.core.publisher.Mono;
    
    @Configuration
    public class RateLimiterConfig {
    
        // 根据IP地址限流
        @Bean
        public KeyResolver ipKeyResolver() {
            return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
        }
    
        // 根据用户ID限流 (需要从请求头或JWT中获取)
        // @Bean
        // public KeyResolver userKeyResolver() {
        //     return exchange -> Mono.just(exchange.getRequest().getHeaders().getFirst("X-User-Id"));
        // }
    }
  3. application.yml配置:

    spring:
      cloud:
        gateway:
          routes:
            - id: your_route
              uri: lb://your-service
              predicates:
                - Path=/api/**
              filters:
                - name: RequestRateLimiter
                  args:
                    redis-rate-limiter.replenishRate: 10 # 每秒补充令牌数量
                    redis-rate-limiter.burstCapacity: 20 # 令牌桶容量
                    key-resolver: '#{@ipKeyResolver}' # 使用哪个KeyResolver
    • redis-rate-limiter.replenishRate:每秒补充令牌数量。
    • redis-rate-limiter.burstCapacity:令牌桶容量。
    • key-resolver:使用哪个KeyResolver。

Zuul + Redis + Lua脚本实现令牌桶 (以Zuul 1.x为例):

  1. 引入依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  2. Lua脚本 (rate_limit.lua):

    local key = KEYS[1]
    local rate = tonumber(ARGV[1])
    local burst = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    local ttl = tonumber(ARGV[4])
    
    local function exists(key)
        return redis.call('exists', key) == 1
    end
    
    local function pttl(key)
        return redis.call('pttl', key)
    end
    
    local function setex(key, ttl, value)
        return redis.call('setex', key, ttl, value)
    end
    
    local function incr(key)
        return redis.call('incr', key)
    end
    
    -- 初始化令牌桶
    if not exists(key) then
        setex(key, ttl, burst)
        return 1
    end
    
    -- 判断令牌是否足够
    local tokens = tonumber(redis.call('get', key))
    if tokens <= 0 then
        return 0
    end
    
    -- 消耗令牌
    local new_tokens = tokens - 1
    redis.call('setex', key, ttl, new_tokens)
    return 1
  3. RateLimiter Filter:

    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.scripting.support.ResourceScriptSource;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import javax.servlet.http.HttpServletRequest;
    import java.util.Collections;
    import java.util.List;
    
    @Component
    public class RateLimitFilter extends ZuulFilter {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Value("${rate-limit.replenishRate}")
        private int replenishRate;
    
        @Value("${rate-limit.burstCapacity}")
        private int burstCapacity;
    
        private DefaultRedisScript<Long> script;
    
        @PostConstruct
        public void init() {
            script = new DefaultRedisScript<>();
            script.setScriptSource(new ResourceScriptSource(new ClassPathResource("rate_limit.lua")));
            script.setResultType(Long.class);
        }
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 0;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
    
            String ipAddress = request.getRemoteAddr(); // 根据IP地址限流
            String key = "rate_limit:" + ipAddress;
    
            List<String> keys = Collections.singletonList(key);
            Long result = redisTemplate.execute(script, keys, String.valueOf(replenishRate), String.valueOf(burstCapacity), String.valueOf(System.currentTimeMillis() / 1000), String.valueOf(60));
    
            if (result != null && result == 0) {
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(429); // Too Many Requests
                ctx.setResponseBody("Too Many Requests");
                return null;
            }
    
            return null;
        }
    }
  4. application.yml配置:

    rate-limit:
      replenishRate: 10 # 每秒补充令牌数量
      burstCapacity: 20 # 令牌桶容量

五、Routing:指路明灯,找到心仪的妃子

API Gateway的第三道防线就是Routing,根据请求的路径或其他特征,将请求转发到对应的微服务。

Spring Cloud Gateway的Routing方式:

  • 基于Path: 根据请求路径进行路由。
  • 基于Header: 根据请求头进行路由。
  • 基于Query Parameter: 根据请求参数进行路由。
  • 基于Method: 根据请求方法进行路由。
  • 自定义Predicate: 使用自定义的Predicate进行路由。

Spring Cloud Gateway路由配置示例 (application.yml):

spring:
  cloud:
    gateway:
      routes:
        - id: user_service
          uri: lb://user-service # 使用LoadBalancerClient,转发到名为 user-service 的微服务
          predicates:
            - Path=/user/** # 匹配 /user/** 的请求
        - id: order_service
          uri: lb://order-service # 使用LoadBalancerClient,转发到名为 order-service 的微服务
          predicates:
            - Path=/order/** # 匹配 /order/** 的请求
            - Header=X-Request-Source,mobile # 匹配请求头 X-Request-Source 为 mobile 的请求

Zuul的Routing方式:

  • 静态路由: 在配置文件中指定路由规则。
  • 动态路由: 从注册中心 (例如Eureka) 动态获取服务列表,并进行路由。

Zuul路由配置示例 (application.yml):

zuul:
  routes:
    user-service: # 路由名称
      path: /user/** # 匹配 /user/** 的请求
      serviceId: user-service # 转发到名为 user-service 的微服务 (使用服务发现)
    order-service: # 路由名称
      path: /order/** # 匹配 /order/** 的请求
      url: http://order-service:8081 # 转发到的微服务地址 (不推荐直接写死,建议使用服务发现)

六、总结

API Gateway是微服务架构中至关重要的一环,它可以有效地保护后端服务,提高系统的可扩展性和安全性。Spring Cloud GatewayZuul都是优秀的API Gateway解决方案,选择哪个取决于你的具体需求和技术栈。

希望今天的讲座对大家有所帮助,有问题欢迎提问!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注