一、网关简介
-
网关介绍
在微服务架构里,服务的粒度被细化,各业务系统都可独立的开发部署,甚至不同的语言编写,这时候就需要一台和语言无关的服务协议作为各单元的通讯方式,API 网关是对所有的调用者透明,对与提供rest api的服务有所保护,隐藏于api网关之后只关注服务的创建,不关注这些策略的基础设施。
-
网关职能
-
网关分类功能
二、Gateway的路由
-
简介
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
-
主要概念
-
Route(路由):路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。
-
Predicate(断言):路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等,写法必须遵循 key=vlue的形式
-
Filter(过滤器):过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容
-
-
静态配置
-
配置文件方式
spring: application: name: bike-gateway cloud: gateway: ## 开启从注册中心动态创建路由的功能,利用微服务进行路由,如果配置为true会基于注册中心心跳刷新路由 discovery: locator: enabled: false # routes: # - id: business_route #路由id没有规则要求唯一,建议配合服务名使用 # uri: lb://bike-business #微服务名称进行服务路由,规则:lb://服务名 # predicates: # - Path=/business/** #断言,路劲相匹配的进行路由 # # - id: data_route #路由id没有规则要求唯一,建议配合服务名使用 # uri: lb://bike-data #微服务名称进行服务路由,规则:lb://服务名 # predicates: # - Path=/data/** #断言,路劲相匹配的进行路由 复制代码
-
配置类方式
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder, AddRequestHeaderGatewayFilterFactory throttle) { NameValueConfig requestHeader = new NameValueConfig(); requestHeader.setName("routes-a"); requestHeader.setValue("yes"); return builder.routes() .route(r -> r.host("localhost:8080").and().path("/routes-api/a/**") .filters(f ->{ f.addResponseHeader("X-TestHeader", "foobar"); return f.redirect(HttpStatus.MOVED_PERMANENTLY.value(), "http://www.xinyues.com"); }) .uri("http://localhost:8080").id("custom-1") ) .route(r -> r.path("/routes-api/b/**") .filters(f -> f.addResponseHeader("X-AnotherHeader", "baz")) .uri("http://localhost:8080").id("custom-2") ) .route(r -> r.order(0) .host("localhost:8080").and().path("/routes-api/c/**") .filters(f -> f.filter(throttle.apply(requestHeader))) .uri("http://localhost:8080").id("custom-3") ) .build(); } 复制代码
-
-
动态路由
-
数据准备
创建路由表&新增两条路由数据
CREATE TABLE `route_info` ( `id` varchar(60) NOT NULL COMMENT '主键', `routeName` varchar(10) NOT NULL COMMENT '路由名称', `predicates` varchar(50) NOT NULL COMMENT '断言', `uri` varchar(255) NOT NULL COMMENT '服务uri', `orders` int DEFAULT NULL COMMENT '路由顺序', `createTime` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `bike-demo`.`route_info`(`id`, `routeName`, `predicates`, `uri`, `orders`, `createTime`) VALUES ('bike-business-route', 'business', 'Path=/bike-business/**', 'lb://bike-business', 0, '2021-04-26 16:32:20'); INSERT INTO `bike-demo`.`route_info`(`id`, `routeName`, `predicates`, `uri`, `orders`, `createTime`) VALUES ('bike-data-route', 'data', 'Path=/bike-data/**', 'lb://bike-data', 1, '2021-04-26 16:33:08'); 复制代码
-
源码分析
根据springboot的自动装配,我们需要找到*AutoConfiguration自动装配类,对于gateway也不例外,我们需要找到GatewayAutoConfiguration配置类,该类里面会配置了一个InMemoryRouteDefinitionRepository根据该类的继承关系,最终找到了RouteDefinitionRepository接口,他的父接口提供了
-
//新增路由 Mono<Void> save(Mono<RouteDefinition> route); 复制代码
-
//删除路由 Mono<Void> delete(Mono<String> routeId); 复制代码
-
//获取路由 Flux<RouteDefinition> getRouteDefinitions(); 复制代码
如果要动态装配,那么我们就可以继承RouteDefinitionRepository重写这些方法即可
GatewayAutoConfiguration自动装配
@Configuration( proxyBeanMethods = false ) @ConditionalOnProperty( name = {"spring.cloud.gateway.enabled"}, matchIfMissing = true ) @EnableConfigurationProperties @AutoConfigureBefore({HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class}) @AutoConfigureAfter({GatewayReactiveLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class}) @ConditionalOnClass({DispatcherHandler.class}) public class GatewayAutoConfiguration { //默认的配置 @Bean @ConditionalOnMissingBean({RouteDefinitionRepository.class}) public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() { return new InMemoryRouteDefinitionRepository(); } } 复制代码
CustomerRouteDefinitionRepository为我们自定义的动态路由配置方法,可通过redis方式缓存路由信息,该类只是实现了动态路由配置,重写获取、保存、删除路由的方法,每次配置我们需要动态更新,那么就涉及到我们接下来的动态路由的实现类。
/** * @author :Nickels * @date :2021/4/26 * @desc :动态路由配置,重写获取、保存、删除路由的方法 */ @Slf4j @Component public class CustomerRouteDefinitionRepository implements RouteDefinitionRepository { @Autowired StringRedisTemplate redisTemplate; private final String GATEWAY_HASH_KEY = "gateway:route:"; /** * 配置动态路由信息 * @return */ @Override public Flux<RouteDefinition> getRouteDefinitions() { /** * 从数据库获取路由信息,实际可从缓存中获取路由信息 */ List<Object> values = redisTemplate.opsForHash().values(GATEWAY_HASH_KEY); List<RouteDefinition> targets = new ArrayList<>(); values.forEach(v->{ RouteDefinition resource = JSONObject.parseObject(v.toString(), RouteDefinition.class); RouteDefinition routeDefinition = new RouteDefinition(); BeanUtils.copyProperties(resource,routeDefinition); targets.add(routeDefinition); }); return Flux.fromIterable(targets); } /** * 保存路由信息 * @param route * @return */ @Override public Mono<Void> save(Mono<RouteDefinition> route) { route.subscribe(r->{ RouteDefinition target = new RouteDefinition(); BeanUtils.copyProperties(r, target); log.info("save route definition info : {}", JSONObject.toJSONString(target)); redisTemplate.opsForHash().put(GATEWAY_HASH_KEY, target.getId(), JSONObject.toJSONString(target)); }); return Mono.empty(); } /** * 删除路由信息 * @param routeId * @return */ @Override public Mono<Void> delete(Mono<String> routeId) { routeId.subscribe(id->{ log.info("delete route definition id : {}",id); redisTemplate.opsForHash().delete(GATEWAY_HASH_KEY,id); }); return Mono.empty(); } } 复制代码
DynamicRouteService是我们自定义的动态路由实现类,该类实现了动态的新增,刷新、删除路由,并通过DynamicRouteHandler暴露给外部rest接口实现动态配置路由的功能。这里需要注意的是Gateway是基于webflux框架的实现,需要webflux以及响应式的基础,这里需要注意的就是我们在使用Mono和Flux对象去处理逻辑代码时需要订阅subscribe()一下,否则逻辑代码并不会真的执行
/** * @author :Nickels * @date :2021/5/14 * @desc :动态路由的实现:具体实现动态路由的crud操作 */ @Service public class DynamicRouteService implements ApplicationEventPublisherAware, CommandLineRunner { @Autowired RouteInfoRepository routeInfoRepository; @Autowired CustomerRouteDefinitionRepository customerRouteDefinitionRepository; private ApplicationEventPublisher publisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } /** * 新增路由 * @param routeInfo */ public void save(Mono<RouteInfo> routeInfo){ routeInfo.subscribe(p->{ customerRouteDefinitionRepository.save(mapper.apply(p)).subscribe(); }); this.refreshRoutes(); } /** * 删除路由 * @param id */ public void delete(Mono<String> id){ customerRouteDefinitionRepository.delete(id); //根据业务需要删除数据库配置 // id.subscribe(p->{ // routeInfoRepository.deleteById(p); // }); this.refreshRoutes(); } /** * 刷新路由 */ public void refreshRoutes(){ this.publisher.publishEvent(new RefreshRoutesEvent(this)); } /** * 项目启动时缓存路由信息 * @param args * @throws Exception */ @Override public void run(String... args) throws Exception { this.save(); } /** * 从数据库缓存数据 */ public void save(){ Flux<RouteInfo> all = routeInfoRepository.findAll(); all.subscribe(p->{ customerRouteDefinitionRepository.save(mapper.apply(p)).subscribe(); }); this.refreshRoutes(); } Function<RouteInfo,Mono<RouteDefinition>> mapper = p->{ RouteDefinition routeDefinition = new RouteDefinition(); routeDefinition.setId(p.getId()); routeDefinition.setUri(URI.create(p.getUri())); routeDefinition.setOrder(1); PredicateDefinition predicateDefinition = new PredicateDefinition(p.getPredicates()); routeDefinition.setPredicates(Arrays.asList(predicateDefinition)); return Mono.just(routeDefinition); }; } 复制代码
/** * @author :Nickels * @date :2021/5/14 * @desc :动态路由配置rest接口 */ @Slf4j @RestController @RequestMapping public class DynamicRouteHandler{ @Autowired DynamicRouteService dynamicRouteService; /** * 刷新路由 * @param request * @return */ @GetMapping("/refresh") public Mono<String> refreshRoutes(ServerRequest request){ dynamicRouteService.refreshRoutes(); return Mono.create(MonoSink::success); } /** * 删除路由 * @param id * @return */ @DeleteMapping("/{id}") public Mono<String> delete(@PathVariable String id){ dynamicRouteService.delete(Mono.just(id)); return Mono.just("SUCCESS"); } /** * 新增路由 * @param routeInfoMono * @return */ @PostMapping public Mono<String> save(@RequestBody Mono<RouteInfo> routeInfoMono){ dynamicRouteService.save(routeInfoMono); return Mono.create(MonoSink::success); } } 复制代码
-
-
三、Gateway的过滤
-
简介
客户端向 Spring Cloud Gateway 发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关 Web 处理程序。此处理程序通过特定于请求的过滤器链运行请求。过滤器被虚线分隔的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有“预”过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。从官方文档可知gateway内部是有许许多多的过滤器链处理请求的。
-
GatewayFilter Factories(内置局部过滤器)
-
Spring Cloud Gateway 包含许多内置的 GatewayFilter 工厂。官方文档大概提供了30多种GatewayFilter 工厂,在工作中我们可以选择性的进行配置,基本能满足日常的业务需要,他们都实现了GatewayFilterFactory接口,如果我们有特别需要自定义,也可以实现该接口实现自己的业务逻辑,我们可根据官方文档配置GatewayFilterFactory的具体实现,每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory 结尾。
-
AddRequestHeaderGatewayFilterFactory:根据名称可看出是往请求头里加一些参数的过滤。
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory { public AddRequestHeaderGatewayFilterFactory() { } public GatewayFilter apply(NameValueConfig config) { return new GatewayFilter() { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String value = ServerWebExchangeUtils.expand(exchange, config.getValue()); ServerHttpRequest request = exchange.getRequest().mutate().headers((httpHeaders) -> { //通过配置文件添加header参数 httpHeaders.add(config.getName(), value); }).build(); return chain.filter(exchange.mutate().request(request).build()); } public String toString() { return GatewayToStringStyler.filterToStringCreator(AddRequestHeaderGatewayFilterFactory.this).append(config.getName(), config.getValue()).toString(); } }; } } 复制代码
配置示例:
spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org filters: - AddRequestHeader=X-Request-red, blue 复制代码
该配置是将X-Request-red:blue请求头添加到所有下属过滤器请求头中,参数值还可以通过占位符的方式动态设置,配置示例:
spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org predicates: - Path=/red/{segment} filters: #{segment}通过占位符动态设置segment参数值 - AddRequestHeader=X-Request-Red, Blue-{segment} 复制代码
-
内置局部过滤器
-
-
GlobalFilter(内置全局过滤器)
-
当请求与路由匹配时,过滤 Web 处理程序会将 的所有实例
GlobalFilter
和所有特定GatewayFilter
于路由的实例添加到过滤器链中。这个组合过滤器链是按org.springframework.core.Ordered
接口排序的,你可以通过实现getOrder()
方法来设置(或许也可以通过过滤器@Order注解来指定排序,待验证)。用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。 -
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”。
PRE : 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST :这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。 -
自定义过滤器配置示例:
@Bean public GlobalFilter customFilter() { return new CustomGlobalFilter(); } //自定义全局过滤器通常需实现GlobalFilter和Ordered两个接口 public class CustomGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("custom global filter"); return chain.filter(exchange); } //过滤器执行顺序,值越小优先级越高 @Override public int getOrder() { return -1; } } 复制代码
-
内置全局过滤器
内置过滤器链处理逻辑:
-
四、Gateway的鉴权
-
鉴权逻辑
-
通过对过滤器的学习我们知道可以通过自定义全局过滤器来实现一些拦截操作,那么我们就通过一个自定义拦截器实现鉴权处理逻辑。
-
当客户端第一次请求服务时,服务端对用户进行信息认证(登录),认证通过,将用户信息进行加密形成 token,返回给客户端,作为登录凭证,以后每次请求,客户端都携带认证的 token,服务端对 token进行解密,判断是否有效。这是常用的鉴权模式。
-
-
代码实现
@Component @Slf4j public class AuthenticationFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("my AuthenticationFilter is carried out ........"); String access_token = exchange.getRequest().getHeaders().getFirst("access_token"); if (StringUtils.isEmpty(access_token)){ log.info("authentication is fail ......."); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); //请求结束 } return chain.filter(exchange); } //设置执行顺序,值越小,优先级越高 @Override public int getOrder() { return -2; } } 复制代码
-
五、Gateway的限流
-
简介
在高并发环境下,限流可以保障我们web服务的可用性,也可以防止网络攻击,一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制 MQ 的消费速率。另外还可以根据网络连接数、网络流量、CPU 或内存负载等来限流。
-
限流算法
做限流 (Rate Limiting/Throttling) 的时候,除了简单的控制并发,如果要准确的控制 TPS,简单的做法是维护一个单位时间内的 Counter,如判断单位时间已经过去,则将 Counter 重置零。此做法被认为没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,也就是在两毫秒内发生了两倍的 TPS。
常用的更平滑的限流算法有两种:漏桶算法和令牌桶算法。
-
漏桶算法:漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
-
令牌桶算法:令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。Guava 中的 RateLimiter 采用了令牌桶的算法
-
-
官方时刻
通过对上述过滤器的学习我们知道Gateway内置了很多GatewayFilterFactory,其中RequestRateLimiterGatewayFilterFactory就是用来实现限流功能的。
-
官方释义:
RequestRateLimiterGatewayFilterFactory
采用的是RateLimiter
实现以确定当前请求被允许继续进行。如果不是,HTTP 429 - Too Many Requests
则返回(默认情况下)状态.我们也可以自定义RateLimiter的实现,在配置文件中通过SpEL配置即可 -
KeyResolver:限流策略,我们可以通过实现KeyResolver接口配置限流策略。在配置中,使用 SpEL 按名称引用 bean。
#{@urlKeyResolver}
-
源码
public interface KeyResolver { Mono<String> resolve(ServerWebExchange exchange); } 复制代码
-
配置策略
@Configuration public class CustomerRateLimiter { @Bean("apiKeyResolver") @Primary KeyResolver apiKeyResolver() { //按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态 return exchange -> Mono.just(exchange.getRequest().getPath().toString()); } @Bean("userKeyResolver") KeyResolver userKeyResolver() { //按用户限流 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); } @Bean("ipKeyResolver") KeyResolver ipKeyResolver() { //按IP来限流 return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } } 复制代码
-
-
-
限流实现
-
既然做限流,那么就需要一个存储请求信息的地方,官方推荐了redis,RedisRateLimiter是其配置类,我们可以通过配置指定的参数,来达到符合我们业务预期的限流策略,其使用的事令牌桶的算法。
-
参数介绍
- replenishRate:希望允许用户每秒执行多少请求,也就是令牌桶的填充速率
- burstCapacity:允许用户在一秒内执行的最大请求数,这是令牌桶可以容纳的令牌数量。将此值设置为零会阻止所有请求。
- requestedTokens:请求花费多少令牌。这是每个请求从存储桶中获取的令牌数量,默认为1
-
配置方式:
-
配置文件:
spring: application: name: bike-gateway cloud: gateway: ## 开启从注册中心动态创建路由的功能,利用微服务进行路由,如果配置为true会基于注册中心心跳刷新路由 discovery: locator: enabled: false routes: - id: business_route #路由id没有规则要求唯一,建议配合服务名使用 uri: lb://bike-business #微服务名称进行服务路由,规则:lb://服务名 predicates: - Path=/business/** #断言,路劲相匹配的进行路由(限流时只需要配置需要限流接口) filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 redis-rate-limiter.requestedTokens: 1 # 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName} key-resolver: "#{@apiKeyResolver}" 复制代码
-
配置类
@Configuration public class CustomerRateLimiter { @Autowired RedisRateLimiter redisRateLimiter; @Bean("apiKeyResolver") @Primary KeyResolver apiKeyResolver() { //按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态 return exchange -> Mono.just(exchange.getRequest().getPath().toString()); } @Bean("userKeyResolver") KeyResolver userKeyResolver() { //按用户限流 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); } @Bean("ipKeyResolver") KeyResolver ipKeyResolver() { //按IP来限流 return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } @Bean @Order public RouteLocator routeLocator(RouteLocatorBuilder builder) { return builder.routes() //多个路由规则就添加多个route .route(r -> r.path("/business/**") .filters(f -> f.stripPrefix(1). //添加自定义的filter requestRateLimiter(c -> { c.setKeyResolver(apiKeyResolver()); c.setRateLimiter(redisRateLimiter); c.setStatusCode(HttpStatus.BAD_GATEWAY); }) ) .uri("lb://bike-business") ).build(); } } 复制代码
-
官方地址:Gateway 官方文档
源码地址:源码地址
-