gateway網關
- 系列博客
- 背景
- 一、什么是Spring Cloud Gateway
- 二、為什么要使用Spring Cloud Gateway
- 三、 Spring Cloud Gateway 三大核心概念
- 4.1 Route(路由)
- 4.2 Predicate(斷言)
- 4.3 Filter(過濾)
- 五、Spring Cloud Gateway是如何工作的
- 四、 如何使用Spring Cloud Gateway進行服務路由
- 4.1搭建服務A
- pom文件
- yml配置文件
- 啟動類
- controller類
- 4.2 搭建服務B
- pom文件
- yml配置文件
- 啟動類
- controller類
- 4.3搭建Spring Cloud Gateway服務
- pom文件
- yml配置文件
- 啟動類
- 4.4啟動項目
- 4.5動態路由
- 五、gateway過濾器
- 5.1自定義全局過濾器
- 六、使用gateway做token校驗
- 6.1實現的大致流程示意圖:
- 6.2實現代碼
- gateway服務
- pom.xml文件
- TokenCheckFilter類
- login-service服務
- pom.xml文件
- LoginController類
- 訪問測試
- 總結
- 升華
系列博客
【Spring Cloud一】微服務基本知識
【Spring Cloud 三】Eureka服務注冊與服務發現
【Spring Cloud 四】Ribbon負載均衡
【Spring Cloud 五】OpenFeign服務調用
【Spring Cloud 六】Hystrix熔斷
【Spring Cloud 七】Sleuth+Zipkin 鏈路追蹤
背景
在項目中是使用了Gateway做統一的請求的入口,以及統一的跨域處理以及統一的token校驗。但是這些工作都是之前的同事來做的,正好在新項目中也需要使用其進行統一的token校驗。本著對Gateway更精進一步所以博主就對Gateway進行了較為全面的學習了解,包括動態路由、自定義過濾器、token校驗和續活。
一、什么是Spring Cloud Gateway
Spring Cloud Gateway提供一種簡單有效的方法來路由到對應的API上,并可以提供一些額外的功能,安全性、監控、度量、負載等等。
我們可以這樣理解,Spring Cloud Gateway將該項目中所有服務對外提供的API聚集起來,并向外提供唯一的入口,同時提供了一些額外的功能,安全性、監控、度量、負載等等。
沒使用Spring Cloud Gateway 之前的示意圖:
使用Spring Cloud Gateway之后的示意圖:
二、為什么要使用Spring Cloud Gateway
- 統一入口并解除客戶端與服務端的耦合:在微服務架構中,每個微服務都有自己入口,在沒有它的時候客戶端需要大量微服務的地址,會照成復雜的客戶端代碼。并且會暴露服務端的細節,如果服務端發生變化,可能會影響到客戶端代碼,導致維護困難。
- 動態路由和負載均衡:Spring Cloud GateWay 提供動態路由來應對當;網關來能夠在多個服務實例之間進行負載均衡,提供整個系統的高可用。
- 請求過濾:在微服務架構中,可能需要對請求進行鑒權、身份驗證一個中心化的地方來處理這些請求過濾和處理邏輯可以減少重復的代碼和邏輯。
- 反應式和高性能:由于微服務架構的復雜性,需要一個能夠處理高并發請求的解決方案,以確保系統在高負載情況下保持穩定。Spring Cloud Gateway是基于webFlux框架實現的,而webFlux框架底層使用了高性能Reactor模式通信框架的Netty。
三、 Spring Cloud Gateway 三大核心概念
4.1 Route(路由)
路由是由一個ID、一個目的URI、一組斷言、一組Filter組成。
如果路由斷言為真,那么說明這個路由被匹配上了。
4.2 Predicate(斷言)
是一個java8函數斷言。輸入類型是一個Spring Framewordk ServerWebExchange。可以讓你匹配HTTP上的任何請求。比如請求頭和參數。
4.3 Filter(過濾)
是Spring WebFilter的實例,Spring Cloud Gateway中的Filter分為兩種,分貝是Gateway Filter和Global Filter(一個是針對某一個路由的filter,例如對某一個接口做限流;一個是針對全局的filter,例如token校驗,ip黑名單)。過濾器Filter將會對請求和響應進行修改處理。
五、Spring Cloud Gateway是如何工作的
Spring 官網
客戶端向Spring Cloud Gateway發出請求。如果網關處理器映射器確定請求與路由匹配,則會將其發送到網關web處理器。它通過特定的過濾器鏈來運行請求。過濾器被虛線分割的原因是過濾器可以在發送代理請求之前和之后運行對應的邏輯。
四、 如何使用Spring Cloud Gateway進行服務路由
示例項目示意圖:
備注:Eureka的搭建可以參考這篇博客:【Spring Cloud 三】Eureka服務注冊與服務發現
這里之所以使用Eureka是為了之后做動態路由和負載均衡。
4.1搭建服務A
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.wangwei</groupId><artifactId>login-service</artifactId><version>0.0.1-SNAPSHOT</version><name>login-service</name><description>login-service</description><properties><java.version>8</java.version><spring-cloud.version>Hoxton.SR12</spring-cloud.version></properties><dependencies><!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-redis</artifactId>-->
<!-- </dependency>--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
yml配置文件
server:port: 8081spring:application:name: login-serviceeureka:client:service-url:defaultZone: http://localhost:8761/eurekaregister-with-eureka: true #設置為fasle 不往eureka-server注冊,默認為truefetch-registry: true #應用是否拉取服務列表到本地registry-fetch-interval-seconds: 10 #為了緩解服務列表的臟讀問題,時間越短臟讀越少 性能相應的消耗回答instance: #實例的配置instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}hostname: localhost #主機名稱或者服務ipprefer-ip-address: true #以ip的形式顯示具體的服務信息lease-renewal-interval-in-seconds: 10 #服務實例的續約時間間隔
啟動類
@SpringBootApplication
@EnableEurekaClient
public class LoginServiceApplication {public static void main(String[] args) {SpringApplication.run(LoginServiceApplication.class, args);}}
controller類
@RestController
public class LoginController {@GetMapping("doLogin")public String doLogin(){return "登陸成功";}
}
4.2 搭建服務B
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.wangwei</groupId><artifactId>teacher-service</artifactId><version>0.0.1-SNAPSHOT</version><name>teacher-service</name><description>teacher-service</description><properties><java.version>8</java.version><spring-cloud.version>Hoxton.SR12</spring-cloud.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
yml配置文件
server:port: 8082spring:application:name: teacher-serviceeureka:client:service-url:defaultZone: http://localhost:8761/eurekaregister-with-eureka: true #設置為fasle 不往eureka-server注冊,默認為truefetch-registry: true #應用是否拉取服務列表到本地registry-fetch-interval-seconds: 10 #為了緩解服務列表的臟讀問題,時間越短臟讀越少 性能相應的消耗回答instance: #實例的配置instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}hostname: localhost #主機名稱或者服務ipprefer-ip-address: true #以ip的形式顯示具體的服務信息lease-renewal-interval-in-seconds: 10 #服務實例的續約時間間隔
啟動類
@SpringBootApplication
@EnableEurekaClient
public class TeacherServiceApplication {public static void main(String[] args) {SpringApplication.run(TeacherServiceApplication.class, args);}}
controller類
@RestController
public class TeacherController {@GetMapping("teach")public String teach(){return "教書學習";}
}
4.3搭建Spring Cloud Gateway服務
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.wangwei</groupId><artifactId>gateway-server</artifactId><version>0.0.1-SNAPSHOT</version><name>gateway-server</name><description>gateway-server</description><properties><java.version>8</java.version><spring-cloud.version>Hoxton.SR12</spring-cloud.version></properties><dependencies>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-redis</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-redis-reactive</artifactId>-->
<!-- </dependency>--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
yml配置文件
server:port: 81 #?????80spring:application:name: gateway-servercloud:gateway:enabled: trueroutes:- id: login-service-route # 路由的id 保持唯一uri: http://localhost:8081 #uri統一資源標識符 url 統一資源定位符#uri: lb://login-service #??lb負載均衡predicates: # 斷言是給某一個路由來設定的一種匹配規則 默認不能作用在動態路由上- Path=/doLogin # 匹配規則 只要你Path配置上了/doLogin 就往uri轉發并將路徑帶上- id: teacher-service-routeurl: http://localhost:8082predicates:- Path=/teacheureka:client:service-url:defaultZone: http://localhost:8761/eurekaregister-with-eureka: true #設置為fasle 不往eureka-server注冊,默認為truefetch-registry: true #應用是否拉取服務列表到本地registry-fetch-interval-seconds: 10 #為了緩解服務列表的臟讀問題,時間越短臟讀越少 性能相應的消耗回答instance: #實例的配置instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}hostname: localhost #主機名稱或者服務ipprefer-ip-address: true #以ip的形式顯示具體的服務信息lease-renewal-interval-in-seconds: 10 #服務實例的續約時間間隔
啟動類
@SpringBootApplication
@EnableEurekaClient
public class GatewayServerApplication {public static void main(String[] args) {SpringApplication.run(GatewayServerApplication.class, args);}}
4.4啟動項目
依次啟動Eureka服務,gateway服務和兩個服務A和服務B。
進行調用服務A和服務B:
如下圖所示通過gateway的ip+端口+路徑調用到對應的服務A中和服務B中。
4.5動態路由
在微服務中通常一個服務的實例有多個,那我們網關如何做負載均衡呢?
gateway幫我們做了很多東西,只要我們集成注冊中心(nacos、Eureka、zoomkeeper)并添加對應的配置gateway就可以自動幫我們進行負載均衡。
方式一:
添加對應的配置信息,來開啟動態路由
gateway:enabled: trueroutes:- id: login-service-route # 路由的id 保持唯一uri: http://localhost:8081 #uri統一資源標識符 url 統一資源定位符#uri: lb://login-service #??lb負載均衡predicates: # 斷言是給某一個路由來設定的一種匹配規則 默認不能作用在動態路由上- Path=/doLogin # 匹配規則 只要你Path配置上了/doLogin 就往uri轉發并將路徑帶上- id: teacher-service-routeuri: http://localhost:8082predicates:- Path=/teachdiscovery:locator:enabled: true #開啟動態路由 開啟通過應用名稱找到服務的功能lower-case-service-id: true # 開啟服務名稱小寫
請求服務時需要帶上對應的服務名稱
方式二:
添加對應的配置:uri: lb://login-service #lb負載均衡
gateway:enabled: trueroutes:- id: login-service-route # 路由的id 保持唯一#uri: http://localhost:8081 #uri統一資源標識符 url 統一資源定位符uri: lb://login-service #lb負載均衡predicates: # 斷言是給某一個路由來設定的一種匹配規則 默認不能作用在動態路由上- Path=/doLogin # 匹配規則 只要你Path配置上了/doLogin 就往uri轉發并將路徑帶上- id: teacher-service-routeuri: http://localhost:8082predicates:- Path=/teach
推薦使用方式一進行統一的配置負載均衡。
五、gateway過濾器
過濾器按照作用范圍可以分為兩種,Gateway Filter和Global Filter。
Gateway Filter:網關過濾器需要通過 spring.cloud.routes.filters 配置在具體路由下,只作用在當前路由上或通過 spring.cloud.default-filters 配置在全局,作用在所有路由上。
Global Filter:全局過濾器,不需要配置路由,系統初始化作用在所有路由上。
全局過濾器一般用于:統計請求次數、限流、token校驗、ip黑名單攔截等。
5.1自定義全局過濾器
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {/*** 過濾的方法* 職責鏈模式* 網關里面有使用 mybatis的二級緩存有變種職責鏈模式* @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//針對請求的過濾 拿到請求 header url 參數。。。ServerHttpRequest request = exchange.getRequest();//HttpServletRequest 這個是web里面的//ServerHttpRequest webFlux里面 響應式里面的String path=request.getURI().getPath();System.out.println(path);HttpHeaders headers=request.getHeaders();System.out.println(headers);String name = request.getMethod().name();String hostString = request.getHeaders().getHost().getHostString();System.out.println(hostString);//響應相關數據ServerHttpResponse response = exchange.getResponse();//微服務 肯定是前后端分離的 一般前后端通過數據傳輸是json格式//{"code":200,"msg":"ok"}//設置編碼 響應頭response.getHeaders().set("content-type","application/json;charset=utf-8");//組裝業務返回值HashMap<String ,Object> map=new HashMap<>(4);map.put("code", HttpStatus.UNAUTHORIZED.value());map.put("msg","你未授權");ObjectMapper objectMapper=new ObjectMapper();//把一個map轉換為字節byte[] bytes = new byte[0];try {bytes = objectMapper.writeValueAsBytes(map);} catch (JsonProcessingException e) {throw new RuntimeException(e);}//通過buffer工廠將字節數組包裝成一個數據報DataBuffer wrap = response.bufferFactory().wrap(bytes);return response.writeWith(Mono.just(wrap));//放行,到下一個過濾器//return chain.filter(exchange);}/*** 制定順序的方法* 越小越先執行* @return*/@Overridepublic int getOrder() {return 0;}
}
請求接口進行訪問,可以發現當前請求已經被攔截下來。
六、使用gateway做token校驗
6.1實現的大致流程示意圖:
6.2實現代碼
gateway服務
pom.xml文件
在pom文件中新增redis的依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
注意博主是在本機安裝的redis所以不需要在服務中添加對應的redis配置。
TokenCheckFilter類
新建TokenCheckFilter類并實現全局過濾器和Ordered
package com.wangwei.gatewayserver.filter;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.util.Arrays;
import java.util.HashMap;
import java.util.List;@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {/*** 指定好放行的路徑,白名單*/public static final List<String> ALLOW_URL= Arrays.asList("/doLogin","/myUrl");@Autowiredprivate StringRedisTemplate redisTemplate;/*** 和前端約定好 一般放在請求頭類里面一般key為 Authorization value bearer token* 1.拿到請求url* 2.判斷放行* 3.拿到請求頭* 4.拿到token* 5.校驗* 6.放行/攔截* @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();String path = request.getURI().getPath();if (ALLOW_URL.contains(path)) {return chain.filter(exchange);}//檢查List<String> authorization = request.getHeaders().get("Authorization");if (!CollectionUtils.isEmpty(authorization)){String token = authorization.get(0);if(StringUtils.hasText(token)){//約定好的有前綴的bearer tokenString realToken = token.replaceFirst("bearer ", "");if (StringUtils.hasText(realToken)&&redisTemplate.hasKey(realToken)){return chain.filter(exchange);}}}//攔截ServerHttpResponse response = exchange.getResponse();response.getHeaders().set("content-type","application/json;charset=utf-8");//組裝業務返回值HashMap<String ,Object> map=new HashMap<>(4);map.put("code", HttpStatus.UNAUTHORIZED);map.put("msg","未授權");ObjectMapper objectMapper=new ObjectMapper();//把一個map轉換為字節byte[] bytes = new byte[0];try {bytes = objectMapper.writeValueAsBytes(map);} catch (JsonProcessingException e) {throw new RuntimeException(e);}//通過buffer工廠將字節數組包裝成一個數據報DataBuffer wrap = response.bufferFactory().wrap(bytes);return response.writeWith(Mono.just(wrap));}@Overridepublic int getOrder() {return 0;}
}
login-service服務
pom.xml文件
在pom文件中新增redis的依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
注意博主是在本機安裝的redis所以不需要在服務中添加對應的redis配置。
LoginController類
在LoginController類中新增doLogin方法
@Autowiredpublic StringRedisTemplate redisTemplate;@PostMapping("doLogin")public String doLogin(@RequestBody User user){//tokenString token= UUID.randomUUID().toString();//存起來redisTemplate.opsForValue().set(token,user.toString(), Duration.ofSeconds(7200));return token;}
訪問測試
http://localhost:81/doLogin
由于在gateway將該/doLogin放入了白名單,所以該請求不會進行token校驗,發送請求成功之后會返回token
訪問http://localhost:81/teach,并在請求頭中添加token,最后可以看到請求訪問成功。
如果我們不在請求頭中添加對應的token或者token為錯誤token,那么gateway會將請求進行攔截。
總結
- gateway在微服務中起到了很重要的作用,作為項目請求的統一入口。
- gateway也體現了我們軟件設計的復用思想,可以統一進行跨域處理,進行token校驗。
- gateway比較重要的一塊是關于他的過濾器,通過實現過濾器的接口,可以自定義過濾器,提供了很強大的可擴展支持
升華
通過學習gateway不光是學習gateway更重要的是學習軟件設計思想。