转自极客时间,仅供非商业用途或交流学习使用,如有侵权请联系删除
你好,我是姚秋辰。
在上节课中,我们了解了如何在Spring Cloud Gateway中加载一个路由,以及常用的内置谓词都有哪些。今天我们就来动手实践一把,在实战项目中搭建一个Gateway网关,并完成三个任务:设置跨域规则、添加路由和实现网关层限流。这三个任务将以怎样的方式展开呢?
首先是跨域规则,它是一段添加在配置文件中的逻辑。我将在编写网关配置文件的同时,顺便为你讲解下跨域的基本原理,以及如何设置同源访问策略。
然后,我将使用基于Java代码的方式来定义静态路由规则。当然了,你也可以使用配置文件来编写路由,用代码还是用配置全凭个人喜好。不过呢,如果你的路由规则比较复杂,比如,它包含了大量谓词和过滤器,那么我还是推荐你使用代码方式,可读性高,维护起来也容易一些。
最后就是网关层限流,我们将使用内置的Lua脚本,并借助Redis组件来完成网关层限流。
闲话少叙,我们先去搭建一个微服务网关应用吧。你可以在Gitee代码仓库中找到下面所有源码。
创建微服务网关
微服务网关是一个独立部署的平台化组件,我们先在middleware目录下创建一个名为gateway的子模块。接下来的工作就是按部就班地搞定依赖项、配置项和路由规则。
添加依赖项
我们要在这个模块的pom.xml文件中添加几个关键依赖项,分别是Gateway、Nacos和Loadbalancer。你可以参考下面的代码。
<dependencies> <!-- Gateway正经依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- Nacos服务发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!-- Redis+Lua限流 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <!-- 其它非关键注解请参考源码 --></dependencies>
这里我只列出了核心依赖项,还有一些辅助依赖组件我没有一一列出,你可以参考源码,查看完整的依赖项列表。
在这几个核心依赖项中,打头的spring-cloud-starter-gateway是最重要的一个,它是实现了网关功能模块的基础组件。而Nacos和Loadbalancer则扮演了“导航”的作用,让Gateway在请求转发的过程中可以通过“服务发现+负载均衡”定位到对应的服务节点。最后一个是Redis依赖项,待会儿我们会用它来实现网关层限流。
虽然我没有把链路追踪组件的相关依赖项添加到Gateway组件中,但是网关通常是一次服务调用的起点,我们在搭建线上应用的时候,应当把Gateway纳入到链路追踪体系当中。所以呢我们需要将Sleuth、Zipkin还有ELK集成进来,我把这个任务留给了你来实现,你可以从依赖项的添加开始,完整回顾一下前面学过的链路追踪知识点,温故而又知新。
依赖项添加完成后,我们接下来去编写bootstrap.yml和application.yml配置文件。
添加配置文件
首先,我们创建一个bootstrap.yml,将“coupon-gateway”定义为当前项目的名称。使用bootstrap.yml的目的之一是优先加载Nacos Config配置项,我们要借助Nacos来完成动态路由表的加载,这部分的内容我将放到下一课再讲。
spring: application: name: coupon-gateway
接下来,我们创建一个application.yml。这个配置文件里的内容主要就两部分,一部分是Nacos服务发现的配置项,这段是老生常谈了咱就不再展开讲了。另一部分是Gateway特有的配置项,我们来看一下。
我会通过Java代码来落地各种路由规则,所以你看到的配置文件并不包含任何路由规则,非常干净清爽。如果你比较喜欢用配置项来定义路由规则,那你可以在spring.cloud.gateway.routes节点下尽情发挥,定义各种路由、谓词断言和过滤器规则。我在上节课开头写了几个在yml中定义路由规则的例子,你可以参考一下。
server: port: 30000spring: # 分布式限流的Redis连接 redis: host: localhost port: 6379 cloud: nacos: # Nacos配置项 discovery: server-addr: localhost:8848 heart-beat-interval: 5000 heart-beat-timeout: 15000 cluster-name: Cluster-A namespace: dev group: myGroup register-enabled: true gateway: discovery: locator: # 创建默认路由,以"/服务名称/接口地址"的格式规则进行转发 # Nacos服务名称本来就是小写,但Eureka默认大写 enabled: true lower-case-service-id: true # 跨域配置 globalcors: cors-configurations: '[/]': # 授信地址列表 allowed-origins: - "http://localhost:10000" - "https://www.geekbang.com" # cookie, authorization认证信息 expose-headers: "" allowed-methods: "" allow-credentials: true allowed-headers: "" # 浏览器缓存时间 max-age: 1000
上面这段配置代码的重点是全局跨域规则,我在spring.cloud.gateway.globalcors下添加了一段跨域规则的相关配置,这里我们就来展开说道说道。
什么是跨域规则
在了解如何配置跨域规则之前,我需要先为你讲一讲什么是浏览器的“同源保护策略”。
如果前后端是分离部署的,大部分情况下,前端系统和后端API都在同一个根域名下,但也有少数情况下,前后端位于不同域名。比如前端页面的地址是geekbang.com,后端API的访问地址是infoq.com。如果一个应用请求发生了跨域名访问,比如位于geekbang.com的页面通过Node.js访问了infoq.com的后端API,这种情况就叫“跨域请求”。
我们的浏览器对跨域访问的请求设置了一道保护措施,在跨域调用发起之前,浏览器会尝试发起一个OPTIONS类型的请求到目标地址,探测一下你的后台服务是否支持跨域调用。如果你的后端Say NO,那么前端浏览器就会阻止这次非同源访问。通过这种方式,一些美女聊天类的钓鱼网站就无法对你实施跨站脚本攻击了,这就是浏览器端的同源保护策略。
不过也有一种例外,比如你的前端网站和后端接口确实部署在了两个域名,而这两个域名背后都是正经应用,这时候为了让浏览器可以通过同源保护策略的检查,你就必须在后台应用里设置跨域规则,告诉浏览器哪些跨域请求是可以被接受的。
我们接下来就来了解一下,如何通过跨域配置的参数来控制跨域访问。这些参数都定义在的spring.cloud.gateway.globalcors.cors-configurations节点的[/*]路径下,[/]这串通配符可以匹配所有请求路径。当然了,你也可以为某个特定的路径设置跨域规则(比如[/order/])。
在这上面的几个配置项中,allowed-origins是最重要的,你需要将受信任的域名添加到这个列表当中。从安全性角度考虑,非特殊情况下我并不建议你使用*通配符,因为这意味着后台服务可以接收任何跨域发来的请求。
到这里,所有配置都已经Ready了,我们可以去代码中定义路由规则了。
定义路由规则
我推荐你使用一个独立的配置类来管理路由规则,这样代码看起来比较清晰。比如我这里就在com.geekbang.gateway下面创建了RoutesConfiguration类,为三个微服务分别定义了简单明了的规则。你可以参考一下这段代码。
@Configurationpublic class RoutesConfiguration { @Bean public RouteLocator declare(RouteLocatorBuilder builder) { return builder.routes() .route(route -> route .path("/gateway/coupon-customer/") .filters(f -> f.stripPrefix(1)) .uri("lb://coupon-customer-serv") ).route(route -> route .order(1) .path("/gateway/template/") .filters(f -> f.stripPrefix(1)) .uri("lb://coupon-template-serv") ).route(route -> route .path("/gateway/calculator/") .filters(f -> f.stripPrefix(1)) .uri("lb://coupon-calculation-serv") ).build(); }}
这三个路由规则都是大同小异的。我们就以第二个路由规则为例,你可以看出,路由设置遵循了一套三连的风格。
首先,我使用path谓词约定了路由的匹配规则为path=“/template/”。这里你要注意的是,如果某一个请求匹配上了多个路由,但你又想让各个路由之间有个先后匹配顺序,这时你就可以使用order(n)方法设定路由优先级,n数字越小则优先级越高。
接下来,我使用了一个stripPrefix过滤器,将path访问路径中的第一个前置子路径删除掉。这样一来,/gateway/template/xxx的访问请求经由过滤器处理后就变成了/template/xxx。同理,如果你想去除path中打头的前两个路径,那就使用stripPrefix(2),参数里传入几它就能吞掉几个prefix path。
最后,我使用uri方法指定了当前路由的目标转发地址,这里的“lb://coupon-template-serv”表示使用本地负载均衡将请求转发到名为“coupon-template-serv”的服务。
在这一套三连里,谓词和uri你是再熟悉不过了,但这个filter想必还是第一次见到。我来带你简单了解一下Gateway Filter的使用方式,再用一个简单的小案例教你借助过滤器来实现基于Lua + Redis的网关层限流。
Filter和网关限流
在第23课中,我们了解了Gateway过滤器在一个Request生命周期中的作用阶段。其实Filter的一大功能无非就是对Request和Response动手动脚,为什么这么说呢?比如你想对Request Header和Parameter进行删改,又或者从Response里面删除某个Header,那么你就可以使用下面这种方式,通过链式Builder风格构造过滤器链。
.route(route -> route .order(1) .path("/gateway/template/") .filters(f -> f.stripPrefix(1) // 修改Request参数 .removeRequestHeader("mylove") .addRequestHeader("myLove", "u") .removeRequestParameter("urLove") .addRequestParameter("urLove", "me") // response系列参数 不一一列举了 .removeResponseHeader("responseHeader") ) .uri("lb://coupon-template-serv")
当然了,Gateway的内置过滤器远不止上面这几个,还包括了redirect转发、retry重试、修改requestBody等等内置Filter。如果你对这些内容感兴趣,你可以根据IDE中自动弹出的代码提示来了解它们,再配几个到路由规则里把玩一下。
接下来,我们通过一个轻量级的网关层限流方案来进一步熟悉Filter的使用,这个限流方案所采用的底层技术是Redis + Lua。
Redis你一定很熟悉了,而Lua这个名词你可能是第一次听说,但提到愤怒的小鸟这个游戏,你一定不陌生,这个游戏就是用Lua语言写的。Lua是一类很小巧的脚本语言,它和Redis可以无缝集成,你可以在Lua脚本中执行Redis的CRUD操作。在这个限流方案中,Redis用来保存限流计数,而限流规则则是定义在Lua脚本中,默认使用令牌桶限流算法。如果你对Lua脚本的内容感兴趣,可以在IDE中全局搜索request_rate_limiter.lua这个文件。
前面我们已经添加了Redis的依赖和连接配置,现在你可以直接来定义限流参数了。我在Gateway模块里新建了一个RedisLimitationConfig类,专门用来定义限流参数。我们用到的主要参数有两个,一个是限流的维度,另一个是限流规则,你可以参考下面的代码。
@Configurationpublic class RedisLimitationConfig { // 限流的维度 @Bean @Primary public KeyResolver remoteHostLimitationKey() { return exchange -> Mono.just( exchange.getRequest() .getRemoteAddress() .getAddress() .getHostAddress() ); } //template服务限流规则 @Bean("tempalteRateLimiter") public RedisRateLimiter templateRateLimiter() { return new RedisRateLimiter(10, 20); } // customer服务限流规则 @Bean("customerRateLimiter") public RedisRateLimiter customerRateLimiter() { return new RedisRateLimiter(20, 40); } @Bean("defaultRateLimiter") @Primary public RedisRateLimiter defaultRateLimiter() { return new RedisRateLimiter(50, 100); }}
我在remoteHostLimitationKey这个方法中定义了一个以Remote Host Address为维度的限流规则,当然了你也可以自由发挥,改用某个请求参数或者用户ID为限流规则的统计维度。其它的三个方法定义了基于令牌桶算法的限流速率,RedisRateLimiter类接收两个int类型的参数,第一个参数表示每秒发放的令牌数量,第二个参数表示令牌桶的容量。通常来说一个请求会消耗一张令牌,如果一段时间内令牌产生量大于令牌消耗量,那么积累的令牌数量最多不会超过令牌桶的容量。
定义好了限流参数之后,我们来看一下如何将限流规则应用到路由表中。
因为Gateway路由规则都定义在RoutesConfiguration类中,所以你需要把刚才我们定义的限流参数类注入到RoutesConfiguration类中。考虑到不同的路由表可能会使用不同的限流参数,所以你在定义多个限流参数的时候,可以使用@Bean(“customerRateLimiter”)这种方式来做区分,然后在Autowired注入对象的时候,使用@Qualifier(“customerRateLimiter”)指定想要加载的限流参数就可以了。
@Autowiredprivate KeyResolver hostAddrKeyResolver;@Autowired@Qualifier("customerRateLimiter")private RateLimiter customerRateLimiter;@Autowired@Qualifier("tempalteRateLimiter")private RateLimiter templateRateLimiter;
限流参数注入完成之后,接下来我们只需要添加一个内置的限流过滤器,分别指定限流的维度、限流速率就可以了,你可以参考下面这段rquestRateLimiter过滤器配置代码。除了限流参数之外,我还额外定义了一个Status Code,当服务请求被限流的时候,后端服务便会返回我指定的这个Status Code。
.route(route -> route.path("/gateway/coupon-customer/") .filters(f -> f.stripPrefix(1) .requestRateLimiter(limiter-> { limiter.setKeyResolver(hostAddrKeyResolver); limiter.setRateLimiter(customerRateLimiter); // 限流失败后返回的HTTP status code limiter.setStatusCode(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED); } ) ) .uri("lb://coupon-customer-serv")
到这里,我们就完整搭建了Gateway组件的路由和限流规则,最后你只需要写一个普通的启动类就可以在本地测试了。接下来我来带你回顾一下这一节的重点内容吧。
总结
今天我们为三个微服务组件设置了路由规则和限流规则。尽管Gateway组件本身提供了丰富的内置谓词和过滤器,但在实际项目中我们大多用不到它们,因为网关层的核心用途只是简单的路由转发,为了保证组件之间的职责隔离,我并不建议通过谓词和过滤器实现带有业务属性的逻辑。
那什么样的逻辑可以在网关层实现呢?比如一些通用的身份鉴权、登录检测和签名验签之类的服务,你可以将这类安全检测的逻辑前置到网关层来实现,这样可以对不合法请求做快速失败处理。
思考题
结合这节课的内容,请你尝试说一说,内置Filter是如何实现的,它继承了哪些通用类和接口。再请你在本地用类似的方式实现一个自定义的过滤器,并配置到路由表中。你可以使用这个过滤器完成一些简单的业务,比如打印所有发到网关服务的请求和响应参数。
好啦,这节课就结束啦。欢迎你把这节课分享给更多对Spring Cloud感兴趣的朋友。我是姚秋辰,我们下节课再见!