各位靓仔靓女,今天咱们来聊聊Java API Gateway的设计,主要围绕Spring Cloud Gateway
和Zuul
,重点攻克Authentication
(认证)、Rate Limiting
(限流)和Routing
(路由)这三大难题。准备好了吗?开始发车!
一、API Gateway:站在门口的大管家
想象一下,你的后宫(微服务集群)佳丽三千,每个妃子(微服务)都有自己的专长。皇帝(前端应用)想要宠幸哪个妃子,总不能直接冲进后宫乱来吧?这时候,就需要一个大管家(API Gateway),负责:
- 验明正身 (Authentication): 确认皇帝是不是真的皇帝,有没有资格宠幸后宫。
- 雨露均沾 (Rate Limiting): 防止皇帝短时间内把某个妃子榨干,保证后宫和谐稳定。
- 指路明灯 (Routing): 引导皇帝准确找到想宠幸的妃子,避免走错房间。
所以,API Gateway的核心作用就是:把外部请求统一入口,进行身份验证、流量控制和路由转发,最终将请求导向后端微服务。
二、两大门神:Spring Cloud Gateway vs. Zuul
目前Java界比较流行的两大API Gateway方案就是Spring Cloud Gateway
和Zuul
。
特性 | 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对象,包含了三个部分:
- Header (头部): 声明Token类型和加密算法。
- Payload (载荷): 存放用户信息和声明 (Claims)。
- Signature (签名): 对Header和Payload进行加密,防止篡改。
JWT认证流程:
- 用户提供用户名和密码登录。
- 服务器验证用户信息,如果正确,生成JWT。
- 服务器将JWT返回给客户端。
- 客户端将JWT保存在本地 (例如LocalStorage或Cookie)。
- 客户端每次请求API时,都在请求头里带上JWT (通常放在Authorization头部,格式为:
Authorization: Bearer <JWT>
)。 - API Gateway接收到请求,验证JWT的有效性。
- 如果JWT有效,则放行请求;否则,拒绝请求。
Spring Cloud Gateway + JWT实现:
-
引入依赖:
<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>
-
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.secret
和jwt.expiration
需要在application.yml
中配置。
-
自定义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()); } }
-
配置路由:
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为例):
-
引入依赖: 同Spring Cloud Gateway
-
JWT工具类: 同Spring Cloud Gateway
-
自定义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; } }
-
配置路由 (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脚本实现令牌桶:
-
引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
-
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")); // } }
-
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为例):
-
引入依赖:
<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>
-
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
-
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; } }
-
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 Gateway
和Zuul
都是优秀的API Gateway解决方案,选择哪个取决于你的具体需求和技术栈。
希望今天的讲座对大家有所帮助,有问题欢迎提问!下次再见!