目录
在一个成熟稳定的微服务架构中,为了保护后端接口安全,避免暴露真实的接口地址,通常在请求到达接口前,会通过一层叫做“网关”的服务,经过网关的代理和转发,再到后端,这就是网关的作用。比如大家熟悉的nginx,gateway,zuul等,可以说在互联网公司产品中都有使用,网关有哪些作用呢?
可以说,网关在一个系统中承载着非常重要的作用:
随着微服务架构越来越普遍,网关在整改架构中的地位也越来越重要,伴随着云原生的兴起与大规模落地实践,也逐渐开始出现各种新型的网关。
springcloud微服务体系下,仍然可以考虑传统的网关,如下:
随着k8s的大规模运用实践,一些与之相关的网关也开始出现,比如:
在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去用。

上述这样的架构,会存在着诸多的问题:
每个业务都需要单独进行鉴权、限流、权限校验、跨域等逻辑,每个业务都各自为战,自己造轮子实现一遍;
如果业务量比较简单的话,暂时还不会有什么问题,随着业务越来越复杂,一个复杂的页面可能会涉及到数百个微服务协同工作,如果每个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,想象一下你打开一个APP,通过抓包发现涉及到了数百个远程调用,这在移动端下会显得非常低效;
后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造,比如商品服务,随着业务变的越来越复杂,后期需要进行拆分成多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常蛋疼;
针对上面存在的这些问题,可以借助API网关来解决
所谓API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
添加上API网关之后,系统的架构图变成了如下所示

相比第一代网关zuul,Gateway提供了丰富而强大的功能;
1)基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
2)动态路由:能够匹配任何请求属性;
3)支持路径重写;
4)集成 Spring Cloud 服务发现功能(Nacos,Eureka);
5)可集成流控降级功能(Sentinel、Hystrix);
6)可以对路由指定易于编写的 Predicate(断言)和 Filter(过滤器);
路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组断言工厂、一组Filter组成。如果断言为真,则说明请求的URL和 配置的路由匹配。
Java8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange。断言函数允许开发者去定义 匹配Http request中的任何信息,比如请求头和参数等。
SpringCloud Gateway中的filter分为Gateway FilIer和Global Filter。Filter可以对请求和响应进行处理。

根据上图,gateway的工作流程大致如下:
接下来通过一个案例快速体验下gateway的使用
创建一个新的工程,只需要导入这一个依赖即可
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
参考配置文件中的注释进行理解,gateway功能点核心就在于配置文件的使用,通过声明式的配置灵活的控制路由;
server:
port: 8088
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
#当前路由的唯一标识,路由到哪个服务上面去
- id: order-route
uri: http://localhost:8083
predicates:
#即所有以order-serv开头的请求都转发到这个微服务处理
- Path=/order-serv/**
filters:
#访问的时候,Gateway会自动的把order-serv过滤掉,从而转发到对应微服务的正确路径上
#比如客户端访问的是 http://localhost:8088/order-serv/order/add,通过gateway之后实际访问的是http://localhost:8088/order/add
- StripPrefix=1
@SpringBootApplication
public class GatewayApp01 {
public static void main(String[] args) {
SpringApplication.run(GatewayApp01.class,args);
}
}
启动之前的order模块和stock模块,在order模块中有一个 /order/add接口,按照上述的配置,我们访问需要经过gateway的转发,所以完整的请求地址为:http://localhost:8088/order-serv/order/add,如果访问这个地址能够像访问:http://localhost:8083/order/add 那样的效果,说明gateway的配置生效了,浏览器访问接口效果如下:

在上述配置文件中可以发现,直接在配置文件中写死了gateway转发时路径的地址, 这种做法是不规范同时会引发很多问题的,正确而合理的做法是通过指定的服务名称去转发,接下来使用nacos作为注册中心,从注册中心获取此地址;
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
主要有两处,一是添加nacos的地址,而是将转发的uri地址修改为nacos上面的服务名称
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心地址
gateway:
routes:
#当前路由的唯一标识,路由到哪个服务上面去
- id: order-route
uri: lb://order-service #需要转发的服务地址,nacos上注册的那个服务地址
predicates:
#即所有以order-serv开头的请求都转发到这个微服务处理
- Path=/order-serv/**
filters:
#访问的时候,Gateway会自动的把pay过滤掉,从而转发到对应微服务的正确路径上
#比如客户端访问的是 http://localhost:8088/order-serv/order/add,实际访问的是http://localhost:8088/order/add
- StripPrefix=1
启动几个模块的服务之后,再次调用相同的接口,与上面的效果一致

基于整合nacos基础上,gateway的配置文件还可以进一步简化成下面这样,通过这样的配置,当请求地址为: /order-service/order/add接口时,会自动去nacos中查找是否有这样的服务存在;
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心地址
gateway:
discovery:
locator:
enabled: true #是否启动自动发现nacos上面的服务
启动服务后再次测试,仍然可以得到正确的返回结果;
Gateway提供了路由断言工厂的配置,使得对于请求的处理更加的丰富和灵活,开发者只需要在配置文件中根据业务场景做一些规则的设置即可。SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配,如下:
此类型的断言根据时间做判断,主要有三个:
‐ After=2022‐12‐29T23:59:59.789+08:00[Asia/Shanghai]
RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
‐ RemoteAddr=192.168.1.1/24
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求cookie是否具有给定名称且值与正则表达式匹配
‐Cookie=china, ch
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否具有给定名称且值与正则表达式匹配
‐Header=X‐Request‐Id, \d+
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则
‐Host=**.testhost.org
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配
‐ Method = GET
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则
‐Path=/foo/{segment}
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配
‐Query=baz, ba.
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
‐id: weight_route1
uri: host1
predicates:
‐Path=/product/**
‐Weight=group3, 1
‐id: weight_route2
uri: host2
predicates:
‐Path=/product/**
‐Weight= group3, 9
添加如下配置文件,意思是在满足路由转发的规则前提下,还需要时间在配置的时间之后;
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2022-12-29T23:59:59.789+08:00[Asia/Shanghai]
再次启动并访问接口,看的如下效果

添加如下配置,意思是请求的header中还需要携带一个key为username,值为jerry的参数
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2022-12-29T23:59:59.789+08:00[Asia/Shanghai]
- Header=username,jerry
通过postman发起请求,可以看到如下效果

如果去掉header中的参数或者写错了,将会出现404;

添加如下配置,即请求中必须携带name参数
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2022-12-29T23:59:59.789+08:00[Asia/Shanghai]
#- Header=username,jerry
- Query=name
再次请求接口,如果不携带name,将会看的请求404

携带name之后

如果自带的路由断言工厂仍然不能满足你的个性化需求的时候,就可以考虑自定义路由断言工厂。
自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
自定义路由断言工厂注意点:
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
public CheckAuthRoutePredicateFactory() {
super(CheckAuthRoutePredicateFactory.Config.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");
}
public Predicate<ServerWebExchange> apply(CheckAuthRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
public boolean test(ServerWebExchange exchange) {
if(config.getName().equals("hello")){
return true;
}
return false;
}
};
}
//接受配置文件中中的参数
@Validated
@Data
public static class Config {
private String name;
}
}

启动相关的模块服务后再次调用接口,看的如下效果

上面即是说明配置文件中的参数与断言工厂中匹配上之后可以正常路由,如果将配置文件中的参数修改一下

再次测试接口,可以看到下面的效果

从文章开头关于gateway的执行流程图中可以看到,Gateway 内置了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加剔除响应头,添加去除参数等,gateway过滤器详细说明:gateway过滤器说明
过滤器的存在与使用,可以对来源请求进行更加细致的控制,以达到对服务端接口资源进行防护的目的。
作用:为原始请求添加Header,对应的过滤器工厂:AddRequestHeader,修改配置文件
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2022-12-29T23:59:59.789+08:00[Asia/Shanghai]
#- Header=username,jerry
#- Query=name
#- CheckAuth=jerry
filters:
- AddRequestHeader=X-Request-color,red
在OrderController中添加如下测试接口
@GetMapping("/header")
public String header(@RequestHeader("X-Request-color") String color){
return color;
}
浏览器请求接口,可以看到如下效果,说明从gateway中转发过来的请求带上了参数 red;

添加如下配置参数

OrderController添加一个测试接口
@GetMapping("/request-param")
public String testRequestParam(@RequestParam("color")String color) throws Exception {
logger.info("获取请求参数:" + color);
return color;
}
测试接口,可以看到如下效果
6.2 自
某些情况下,如果内置的gateway过滤器还不能满足要求的情况下,可以考虑自定义过滤器,使用也比较简单,自定义类,并继承AbstractNameValueGatewayFilterFactory,且自定义类名称必须要以GatewayFilterFactory结尾并交给spring管理;
@Component
public class CheckAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<CheckAuthGatewayFilterFactory.Config> {
public CheckAuthGatewayFilterFactory() {
super(CheckAuthGatewayFilterFactory.Config.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");
}
public GatewayFilter apply(CheckAuthGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
String paramValue = exchange.getRequest().getQueryParams().getFirst("name");
if (StringUtils.isNotEmpty(paramValue) && paramValue.equals("jerry")) {
return chain.filter(exchange);
} else {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
return exchange.getResponse().setComplete();
}
};
}
@Data
public static class Config {
private String name;
}
}

启动相关的模块服务,通过gateway调用 /order/add接口,如果name参数传入并且value值正确

如果value值不正确,将出现下面的效果

上面的过滤器按照分类来说可认为是局部过滤器,局部过滤器针对某个路由, 需要在路由中进行配置,但是如果实际业务中需要控制的路由比较多的情况下,就需要写很多局部过滤器了,尤其是那些具有相同的需要过滤的场景这样就显得冗余了,此时可以考虑使用全局过滤器
针对所有路由请求,一旦定义就会全局生效,GlobalFilter 接口和 GatewayFilter 有一样的接口定义,只不过, GlobalFilter 会作用于所有路由。
如下列举了官方常用的gateway的全局过滤器

自定义一个类,实现GlobalFilter接口
@Component
@Slf4j
public class LogFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info(exchange.getRequest().getPath().value());
return chain.filter(exchange);
}
}
启动相关的模块服务之后,访问上面的接口,可以看到控制台就输出了请求的路径信息;

跨越这个概念对很多同学来说不陌生,解决跨域的方案有很多种,比如在nginx中进行设置就是比较常见的一种,在Gateway中同样提供了关于解决跨域的方案。

官方文档:gateway跨域配置手册
添加跨域相关的配置信息
server:
port: 8088
spring:
application:
name: api-gateway
# nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
# ----------- 跨域相关的配置
globalcors:
cors-configurations:
'[/**]': #拦截的请求
allowedOrigins: #允许跨域的请求来源
- "http://localhost:8080"
allowedMethods: #运行跨域的请求方式
- "GET"
- "POST"
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2022-12-29T23:59:59.789+08:00[Asia/Shanghai]
#- Header=username,jerry
#- Query=name
#- CheckAuth=jerry
filters:
#- AddRequestHeader=X-Request-color,red
#- AddRequestParameter=color,blue
- CheckAuth=name,jerry
本文通过较大的篇幅详细说明了gateway的使用,更多关于gateway的使用可以参考官网文档进行学习,比如使用gateway进行统一的鉴权、限流等,作为一款优秀的可编程式的服务网关,有必要对其进行深入的学习和了解,才能在架构的设计中合理的进行使用。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h