微服務避免直接暴露地址,需要一個統一入口進行隔離,增強服務調用的安全性。
Spring Cloud Gateway基於Filter鏈提供網關基本功能:安全、監控/埋點、限流等。
Spring Cloud Gateway為微服務架構提供簡單、有效且統一的API路由管理方式。
Spring Cloud Gateway是替代Netflix Zuul(Zuul處於維護狀態,不再進行新功能的開發)的一套解決方案,spring官方推薦我們,如果要用到網關的話使用Gateway代替Zuul.
Spring Cloud Gateway組件的核心是一系列的過濾器,通過這些過濾器可以將客戶端發送的請求轉發(路由)到對應的微服務。
Spring Cloud Gateway是加在整個微服務最前沿的防火牆(過濾)和代理器(路由),隱藏微服務結點IP端口信息,從而加強安全保護。
Spring Cloud Gateway本身也是一個微服務,需要注冊到Eureka服務注冊中心,根據服務的名稱,去Eureka注冊中心查找 服務對應的所有實例列表,然后進行動態路由
Gateway加入后的架構
不管是來自於客戶端(PC或移動端)的請求,還是服務內部調用,一切對服務的請求都可經過網關,然后再由網關來實現 鑒權、動態路由等等操作。Gateway就是我們服務的統一入口。
核心概念
1、路由(routes) 路由信息的組成:由一個ID(可以隨意寫)、一個目的URL、一組斷言工廠、一組Filter組成。如果路由斷言為真,說明請求URL和配置路由匹配。
2、斷言(Predicate) Spring Cloud Gateway中的斷言函數輸入類型是Spring 5.0框架中的ServerWebExchange。Spring Cloud Gateway的斷言函數允許開發者去定義匹配來自於Http Request中的任何信息比如請求頭和參數。
3、過濾器(Filter) 一個標准的Spring WebFilter。 Spring Cloud Gateway中的Filter分為兩種類型的Filter,分別是Gateway Filter(局部過濾器)和Global Filter(全局過濾器)。過濾器Filter將會對請求和響應進行修改處理
路由、斷言、過濾器如下:
Spring cloud: gateway: routes: #路由 # 路由id,可以隨意寫 - id: user-service-route # 代理的服務地址;lb表示從eureka中獲取具體服務 uri: lb://user-service # 路由斷言,可以配置映射路徑 predicates: #斷言 - Path=/** filters: #當前路由下的局部過濾器 # 添加請求路徑的前綴 - PrefixPath=/user
網關的核心功能是:路由和過濾
路由routes
快速入門
需求:PC端通過網關系統將包含有/user的請求路由到http://127.0.0.1:9091/user/用戶id
1、新建maven工程
2、添加依賴:spring-cloud-starter-gateway、spring-cloud-starter-netflix-eureka-client
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3、編寫啟動類。注意添加注解@EnableDiscoveryClient
@SpringBootApplication @EnableDiscoveryClient public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class,args); } }
4、編寫配置文件
server: port: 10010 spring: application: name: api-gateway eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka instance: prefer-ip-address: true
5、編寫路由規則,需要用網關來代理 user-service 服務,先看一下控制面板中的服務狀態:
修改application.yml 文件為:
server: port: 10010 spring: application: name: api-gateway cloud: gateway: routes: # 路由id,可以隨意寫 - id: user-service-route # 代理的服務地址 uri: http://127.0.0.1:9091 # 路由斷言,可以配置映射路徑 predicates: - Path=/user/** eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka instance: prefer-ip-address: true
id前的-表示多個的意思。path=/user/**表示路徑中包含user的話,全部轉到9091這台機器
將符合 Path 規則的一切請求,都代理到 uri 參數指定的地址。本例中,我們將路徑中包含有 /user/** 開頭的請求,代理到http://127.0.0.1:9091
注意Zuul網關的配置與Gateway不一樣:
zuul: routes: manage-course: # 路由名稱,名稱任意,保持所有路由名稱唯一 path: /course/** serviceId: xc-service-manage-course #微服務名稱,網關會從eureka中獲取該服務名稱下的服務實例的地址 # 例子:將請求轉發到http://localhost:31200/course #url: http://www.baidu.com #也可指定url,此url也可以是外網地址\ strip-prefix: false #true:代理轉發時去掉/course/**的前綴,false:代理轉發時不去掉/course/**的前綴 sensitiveHeaders: #默認zuul會屏蔽cookie,cookie不會傳到下游服務,這里設置為空則取消默認的黑名單,如果設置了具體的頭信息則不會傳到下游服務 # ignoredHeaders: 默認為空表示不過慮任何頭
6、啟動測試
訪問的路徑中需要加上配置規則的映射路徑,我們訪問網關:http://localhost:10010/user/8,如果成功的話,會轉到http://127.0.0.1:9091/user/8,從而實現從網關路由到指定的服務。
面向(多個實例的)服務的路由
在剛才的路由規則中,把路徑對應的服務地址寫死了!如果同一服務有多個實例的話,這樣做顯然不合理,可以通過配置動態路由解決。應該根據服務的名稱,去Eureka注冊中心查找 服務對應的所有實例列表,然后進行動態路由!
修改映射配置,通過服務名稱獲取
# 代理的服務地址;lb表示從eureka中獲取具體服務。LoadBalance的縮寫
路由配置中uri所用的協議為lb時(以uri: lb://user-service為例),gateway將使用 LoadBalancerClient把user-service通過eureka解析為實際的主機和端口,並進行ribbon負載均衡。
再次啟動 heima-gateway ,這次gateway進行代理時,會利用Ribbon進行負載均衡訪問:http://localhost:10010/user/8,控制台打印如下內容:
路由的過濾器添加前綴(添加前綴過濾器PrefixPath)PrefixPath GatewayFilter Factory
當客戶端的請求地址和微服務的服務地址不一致的時候,可以通過配置過濾器實現前綴的添加與去除。
可以對請求到網關服務的地址添加或去除前綴。有些時候客戶端的請求地址與微服務中的服務地址路徑並不一定是一樣的,那么可以在網關路由請求地址的時候,可以通過配置過濾器來實現對路徑的調整。
在gateway中可以通過配置路由的過濾器PrefixPath,實現映射路徑中地址的添加;修改application.yml 文件:
spring: cloud: gateway: routes: # 路由id,可以隨意寫 - id: user-service-route # 代理的服務地址 uri: lb://user-service # 路由斷言,可以配置映射路徑 predicates: - Path=/** filters: - PrefixPath=/user
注意:PrefixPath中兩個P均為大寫
通過 PrefixPath=/xxx 來指定了路由要添加的前綴。path=/**表示任意路徑都可以執行路由。注意這里不能寫成/user/**,否則瀏覽器通過地址http://localhost:10010/8訪問網關服務時,無法進入這個路由。
也就是:(訪問網關,通過網關將客戶端發送的請求轉發(路由)到對應的微服務)
PrefixPath=/user http://localhost:10010/8 --》http://localhost:9091/user/8(提供服務的地址),即瀏覽器通過地址http://localhost:10010/8訪問網關服務時,添加前綴/user,變成http://localhost:10010/user/8,然后將符合Path規則(/user/**)的一切請求,都代理到uri參數指定的地址,即http://localhost:9091。
如果前綴有多個,則添加多個就可以了。PrefixPath=/user/abc http://localhost:10010/8 --》http://localhost:9091/user/abc/8
路由的過濾器去除前綴(去除前綴過濾器StripPrefix)StripPrefix GatewayFilter Factory
在gateway中可以通過配置路由的過濾器StripPrefix,實現映射路徑中地址的去除;修改application.yml 文件:
spring: cloud: gateway: routes: # 路由id,可以隨意寫 - id: user-service-route # 代理的服務地址 uri: lb://user-service # 路由斷言,可以配置映射路徑 predicates: - Path=/api/user/** filters: # 1表示過濾1個路徑 2表示過濾兩個路徑,依次類推 - StripPrefix=1
斷言中path會多一個路徑,當路徑中有/api/user時,則會執行這個路由。即一個請求到網關服務,先根據斷言中path來判斷這個地址能不能進入這個路由,如果可以,則再經過過濾器,最后代理到uri指定的地址。
通過 StripPrefix=1 來指定了路由要去掉的前綴個數。如:路徑 /api/user/1 將會被代理到 /user/1 。
也就是:
StripPrefix=1 http://localhost:10010/api/user/8 --》http://localhost:9091/user/8(提供服務的地址)
StripPrefix=2 http://localhost:10010/api/user/8 --》http://localhost:9091/
過濾
過濾器類型:
Gateway實現方式上,有兩種過濾器;
1. 局部過濾器:通過 spring.cloud.gateway.routes.filters 配置在具體路由下,只作用在當前路由上;自帶的過濾器都可以配置或者自定義按照自帶過濾器的方式。如果配置spring.cloud.gateway.default-filters 上會對所有路由生效也算是全局的過濾器;但是這些過濾器(局部過濾器和default-filters)的實現上都是要實現GatewayFilterFactory接口。
2. 全局過濾器:不需要在配置文件中配置,作用在所有的路由上;實現 GlobalFilter 接口即可。
Gateway作為網關的其中一個重要功能,就是實現請求的鑒權。而這個動作往往是通過網關提供的過濾器來實現的。前面的 路由前綴 章節中的功能也是使用過濾器實現的。
Gateway自帶過濾器有幾十個,常見自帶過濾器有:
局部過濾器:
局部過濾器都實現實現GatewayFilterFactory接口,該接口有一個抽象類AbstractGatewayFilterFactory,故局部過濾器可以繼承該抽象類。所有局部過濾器如下:
1、局部過濾器----(也算全局)默認過濾器(default-filters)
默認過濾器對所有路由都生效,也算是全局過濾器,
這些自帶的過濾器可以和使用 路由前綴 章節中的用法類似,也可以將這些過濾器配置成不只是針對某個路由;而是可以對所有路由生效,也就是配置默認過濾器:
If you would like to add a filter and apply it to all routes you can use spring.cloud.gateway.default-filters
. This property takes a list of filters
spring: cloud: gateway: default-filters: - AddResponseHeader=X-Response-Default-Foo, Default-Bar - PrefixPath=/httpbin
本項目實例如下:
spring: cloud: gateway: routes: # 路由id,可以隨意寫 - id: user-service-route # 代理的服務地址 uri: lb://user-service # 路由斷言,可以配置映射路徑 predicates: - Path=/api/user/** filters: # 1表示過濾1個路徑 2表示過濾兩個路徑,依次類推 - StripPrefix=1 default-filters: # 添加響應頭過濾器,對輸出的響應設置其頭部屬性名稱為X-Response-Default-MyName,值為zwh; #如果有多個參數多則重寫一行設置不同的參數 - AddResponseHeader=X-Response-Default-MyName, zwh - AddResponseHeader=X-Response-Default-Mywf, xpp
上述配置后,再訪問 http://localhost:10010/api/user/8 的話;那么可以從其響應中查看到如下信息:
2、局部過濾器(只作用在當前路由上)
1)、AddRequestHeader GatewayFilter Factory
The AddRequestHeader GatewayFilter Factory takes a name and value parameter.
spring: cloud: gateway: routes: - id: add_request_header_route uri: http://example.org filters: - AddRequestHeader=X-Request-Foo, Bar
This will add X-Request-Foo:Bar
header to the downstream request’s headers for all matching requests.
2)、AddRequestParameter GatewayFilter Factory
The AddRequestParameter GatewayFilter Factory takes a name and value parameter.
spring: cloud: gateway: routes: - id: add_request_parameter_route uri: http://example.org filters: - AddRequestParameter=foo, bar
This will add foo=bar
to the downstream request’s query string for all matching requests.
3)、AddResponseHeader GatewayFilter Factory
The AddResponseHeader GatewayFilter Factory takes a name and value parameter.
spring: cloud: gateway: routes: - id: add_request_header_route uri: http://example.org filters: - AddResponseHeader=X-Response-Foo, Bar
This will add X-Response-Foo:Bar
header to the downstream response’s headers for all matching requests.
4)、RemoveRequestHeader GatewayFilter Factory
The RemoveRequestHeader GatewayFilter Factory takes a name
parameter. It is the name of the header to be removed.
spring: cloud: gateway: routes: - id: removerequestheader_route uri: http://example.org filters: - RemoveRequestHeader=X-Request-Foo
This will remove the X-Request-Foo
header before it is sent downstream.
5)、RemoveResponseHeader GatewayFilter Factory
The RemoveResponseHeader GatewayFilter Factory takes a name
parameter. It is the name of the header to be removed.
spring: cloud: gateway: routes: - id: removeresponseheader_route uri: http://example.org filters: - RemoveResponseHeader=X-Response-Foo
This will remove the X-Response-Foo
header from the response before it is returned to the gateway client.
To remove any kind of sensitive header you should configure this filter for any routes that you may want to do so. In addition you can configure this filter once using spring.cloud.gateway.default-filters
and have it applied to all routes.
6)、RequestSize GatewayFilter Factory
The RequestSize GatewayFilter Factory can restrict a request from reaching the downstream service , when the request size is greater than the permissible limit. The filter takes RequestSize
as parameter which is the permissible size limit of the request defined in bytes.
spring: cloud: gateway: routes: - id: request_size_route uri: http://localhost:8080/upload predicates: - Path=/upload filters: - name: RequestSize args: maxSize: 5000000
The RequestSize GatewayFilter Factory set the response status as 413 Payload Too Large
with a additional header errorMessage
when the Request is rejected due to size. Following is an example of such an errorMessage
.
errorMessage
: Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB
Gateway的Filter的執行生命周期
Spring Cloud Gateway 的 Filter 的生命周期也類似Spring MVC的攔截器有兩個:“pre” 和 “post”。“pre”和 “post” 分別會在請求被執行前調用和被執行后調用
這里的 pre 和 post 可以通過過濾器的 GatewayFilterChain 執行filter方法前后來實現。
使用場景
常見的應用場景如下:
1、請求鑒權:一般 GatewayFilterChain 執行filter方法前,如果發現沒有訪問權限,直接就返回空。(執行服務之前進行鑒權)
2、異常處理:一般 GatewayFilterChain 執行filter方法后,記錄異常並返回。(執行服務之后)
3、服務調用時長統計: GatewayFilterChain 執行filter方法前后根據時間統計。
Gateway自定義過濾器
自定義局部過濾器
在application.yml中對某個路由配置過濾器,該過濾器可以在控制台輸出配置文件中指定名稱的請求參數的值。
需求:在過濾器(MyParamGatewayFilterFactory,注意局部過濾器以GatewayFilterFactory結尾,在配置文件中配置MyParam當做自定義過濾器就可以了)中將http://localhost:10010/api/user/8?name=zwh中的參數name的值獲取到並輸出到控制台,並且參數名是可變的,也就是不一定每次都是name,可以通過配置過濾器的時候做到配置參數名
實現步驟:
1、配置過濾器。2、編寫過濾器。3、測試
1、在heima-gateway工程編寫過濾器工廠類MyParamGatewayFilterFactory。繼承AbstractGatewayFilter時,要給AbstractGatewayFilter添加一個泛型,泛型就是一個配置類,這個配置就是要動態接收name這個名稱。由於沒有寫過過濾器,可以參考RemoveResponseHeaderGatewayFilterFactroy.先找到這個類RemoveResponseHeaderGatewayFilterFactory,
public class RemoveResponseHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<NameConfig> {
public RemoveResponseHeaderGatewayFilterFactory() {
super(NameConfig.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");
}
public GatewayFilter apply(NameConfig config) {
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().remove(config.getName());
}));
};
}
}
其中NameConfig類在AbstractGatewayFilterFactory<NameConfig>中定義
public abstract class AbstractGatewayFilterFactory<C> extends AbstractConfigurable<C> implements GatewayFilterFactory<C> { public AbstractGatewayFilterFactory() { super(Object.class); } public AbstractGatewayFilterFactory(Class<C> configClass) { super(configClass); } public static class NameConfig { private String name; public NameConfig() { } public String getName() { return this.name; } public void setName(String name) { this.name = name; } } }
這里,我們在MyParamGatewayFilterFactory類中定義一個內部配置類就可以了。
@Component
public class MyParamGatewayFilterFactory extends AbstractGatewayFilterFactory<MyParamGatewayFilterFactory.Config> {
public MyParamGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (request.getQueryParams().containsKey(config.param)) {
request.getQueryParams().get(config.param) //get方法獲取的是一個List,而且List中只有一個值
.forEach(value ->
System.out.printf("----------局部過濾器-----%s = %s-----", config.param, value));
}
return chain.filter(exchange);
};
}
public static class Config {
// 對應在配置過濾器的時候指定的參數名,如name
private String param;
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
}
}
2、修改配置文件
spring: cloud: gateway: routes: # 路由id,可以隨意寫 - id: user-service-route # 代理的服務地址 uri: lb://user-service # 路由斷言,可以配置映射路徑 predicates: - Path=/api/user/** filters: # 1表示過濾1個路徑 2表示過濾兩個路徑,依次類推 - StripPrefix=1 #自定義過濾器 - MyParam=name
注意:自定義過濾器的命名應該為:***GatewayFilterFactory
測試訪問:http://localhost:10010/api/user/8?name=zwh檢查后台是否輸出name和zwh;
但是若訪問http://localhost:10010/api/user/8?name2=itcast 則是不會輸出的。
Gateway自定義全局過濾器
定義一個全局過濾器檢查請求中是否攜帶有token參數
需求:模擬一個登錄的校驗,編寫全局過濾器,在過濾器中檢查請求地址是否攜帶token參數。如果token參數的值存在,則放行,若果不存在或為空,則設置返回的狀態碼未授權也不再執行下去。
基本邏輯:如果請求中有token參數(注意token只是url中的一個參數,並不是真正意義上的token),則認為請求有效,放行。
1、在zwh-gateway工程編寫全局過濾器類MyGlobalFilter,實現GlobalFilter接口,如果想讓過濾器有先后順序的話,再實現Ordered.
@Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //getFirst方法直接獲取值,而get方法獲取的是List集合 String token = exchange.getRequest().getQueryParams().getFirst("token"); if (StringUtils.isBlank(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// setComplete()方法表示不再執行后面的過濾請求 return exchange.getResponse().setComplete(); } return chain.filter(exchange); // 如果是正常情況下,則繼續執行 } @Override public int getOrder() { //值越小越先執行 return 1; } }
訪問 http://localhost:10010/api/user/8,返回沒有權限。401就表示Unauthorized
訪問 http://localhost:10010/api/user/8?token=abc,能夠進行訪問.
Gateway集成負載均衡和熔斷
Gateway中默認就已經集成了Ribbon負載均衡和Hystrix熔斷機制。但是所有的超時策略都是走的默認值,比如熔斷超時時間只有1S,很容易就觸發了。如果不修改默認值,那么所有的都是走默認值。因此建議手動進行配置:
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 6000 ribbon: ConnectTimeout: 1000 ReadTimeout: 2000 MaxAutoRetries: 0 MaxAutoRetriesNextServer: 0
Gateway的高可用
啟動多個Gateway服務,自動注冊到Eureka,形成集群。這樣的話,網關服務器更加可靠一些。
Ribbon與Nginx的區別
如果是服務內部訪問,訪問Gateway,自動負載均衡(gateway集成Ribbon負載均衡),沒問題。但是,Gateway更多是外部訪問,PC端、移動端等。它們無法通過Eureka進行負載均衡,那么該怎么辦?此時,可以使用其它的服務網關,來對Gateway進行代理。比如:Nginx。即內部服務訪問時,Gateway可以進行負載均衡。PC端、移動端通過Gateway調用微服務時使用Nginx進行負載均衡。微服務使用Feign調用微服務時使用Ribbon進行負載均衡。
Gateway與Feign的區別
Gateway 作為整個應用的流量入口,接收所有的請求,如PC、移動端等,並且將不同的請求轉發至不同的處理微服務模塊,其作用可視為nginx;大部分情況下用作權限鑒定、服務端流量控制。Gateway一般直接給PC、移動端等終端請求調用微服務,而Feign是微服務調用微服務。
Feign 則是將當前微服務的部分服務接口暴露出來,並且主要用於各個微服務之間的服務調用,即Feign用於微服務之間的內部調用。服務之間的調用,由於自己的服務不會威脅到自己的服務,所以可以不經過Gateway。
Gateway跨域配置
一般網關都是所有微服務的統一入口,必然在被調用的時候會出現跨域問題。
跨域:在js請求訪問中,如果訪問的地址與當前服務器的域名、ip或者端口號不一致則稱為跨域請求。只有在前端js發送ajax請求的時候才會出現跨域,如果是java后台端發送請求到其他服務器獲取數據是不會出現跨域請求的,即服務端不存在跨域問題。若不解決則不能獲取到對應地址的返回結果。
如:從在http://localhost:9090中的js訪問 http://localhost:9000的數據,因為端口不同,所以也是跨域請求。
在訪問Spring Cloud Gateway網關服務器的時候,出現跨域問題的話;可以在網關服務器中通過配置解決,允許哪些服務是可以跨域請求的;具體配置如下:
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': #allowedOrigins: * # 這種寫法或者下面的都可以,*表示全部 allowedOrigins: - "http://docs.spring.io" allowedMethods: - GET
上述配置表示:可以允許來自 http://docs.spring.io 的get請求方式獲取服務數據。allowedOrigins 指定允許訪問的服務器地址,如:http://localhost:10000 也是可以的。'[/**]' 表示對所有訪問到網關服務器的請求地址
使用瀏覽器http://localhost:10010/api/user/8?token=abc&name=zwh訪問http://localhost:10010沒有出現跨域,故可以訪問到數據,且全局過濾器和局部過濾器均生效。