微服務網關實戰——Spring Cloud Gateway

做爲Netflix Zuul的替代者,Spring Cloud Gateway是一款很是實用的微服務網關,在Spring Cloud微服務架構體系中發揮很是大的做用。本文對Spring Cloud Gateway常見使用場景進行了梳理,但願對微服務開發人員提供一些幫助。前端

微服務網關SpringCloudGatewayreact

1.概述web

Spring cloud gateway是spring官方基於Spring 5.0、Spring Boot2.0和Project Reactor等技術開發的網關,Spring Cloud Gateway旨在爲微服務架構提供簡單、有效和統一的API路由管理方式,Spring Cloud Gateway做爲Spring Cloud生態系統中的網關,目標是替代Netflix Zuul,其不只提供統一的路由方式,而且還基於Filer鏈的方式提供了網關基本的功能,例如:安全、監控/埋點、限流等。redis

2.核心概念spring

網關提供API全託管服務,豐富的API管理功能,輔助企業管理大規模的API,以下降管理成本和安全風險,包括協議適配、協議轉發、安全策略、防刷、流量、監控日誌等貢呢。通常來講網關對外暴露的URL或者接口信息,咱們統稱爲路由信息。若是研發過網關中間件或者使用過Zuul的人,會知道網關的核心是Filter以及Filter Chain(Filter責任鏈)。Sprig Cloud Gateway也具備路由和Filter的概念。下面介紹一下Spring Cloud Gateway中幾個重要的概念。後端

  • 路由。路由是網關最基礎的部分,路由信息有一個ID、一個目的URL、一組斷言和一組Filter組成。若是斷言路由爲真,則說明請求的URL和配置匹配
  • 斷言。Java8中的斷言函數。Spring Cloud Gateway中的斷言函數輸入類型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的斷言函數容許開發者去定義匹配來自於http request中的任何信息,好比請求頭和參數等。
  • 過濾器。一個標準的Spring webFilter。Spring cloud gateway中的filter分爲兩種類型的Filter,分別是Gateway Filter和Global Filter。過濾器Filter將會對請求和響應進行修改處理

圖片描述

如上圖所示,Spring cloudGateway發出請求。而後再由Gateway Handler Mapping中找到與請求相匹配的路由,將其發送到Gateway web handler。Handler再經過指定的過濾器鏈將請求發送到咱們實際的服務執行業務邏輯,而後返回。跨域

快速入門安全

以Spring Boot框架開發爲例,啓動一個Gateway服務模塊(以Consul做爲註冊中心),一個後端服務模塊。client端請求經gateway服務把請求路由到後端服務。服務器

前提條件:架構

  • Consul:版本1.5.0。
  • Spring bot:版本2.1.5。
  • Spring cloud:版本Greenwich.SR1。
  • Redis:版本5.0.5。

1.微服務開發

這裏以使用Spring Boot框架開發微服務爲例,啓動一個服務並註冊到Consul。

引入依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

註冊服務到Consul,配置文件配置以下:

spring:
  application:
    name: service-consumer
  cloud:
    consul:
      host: 127.0.0.1
      port: 8500
      discovery:
        service-name: service-consumer

以下定義RestController,發佈HTTP接口。

@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;
    @GetMapping(value = "/info")
    public User info() {
        return userService.info();
    }
}

注:此爲服務端配置,經Gateway把請求路由轉發到該服務上。

2.網關配置
建立一個Gateway服務,引入如下依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

啓動類配置以下:

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

Spring Cloud Gateway對client端請求起到路由功能,主要配置以下:

server:
  port: 8098
spring:
  application:
    name: service-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true   
          lower-case-service-id: true  
    consul:
      host: 127.0.0.1 #註冊gateway網關到consul
      port: 8500
      discovery:
        service-name: service-gateway

此時使用http://localhost:8089/service-consumer/user/info訪問服務,網關便可對服務進行路由轉發,把請求轉發到具體後端服務上。此時,url中使用的url前綴service-consumer,是後端服務在Consul註冊的服務名稱轉爲小寫字母之後的字符串。

最佳實踐

01

Gateway網關配置

本文第二部分開發規範中定義了網關進行路由轉發的配置,除了上述配置方式還可使用下面的方式進行配置:

gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
      - id: service_consumer
        uri: lb://service-consumer
        predicates:
        - Path= /consumer/**
        filters:
        - StripPrefix=1

在上面的配置中,配置了一個Path的predicat,將以/consumer/**開頭的請求都會轉發到uri爲lb://service-consumer的地址上,lb://service-consumer(註冊中心中服務的名稱)即service-consumer服務的負載均衡地址,並用StripPrefix的filter 在轉發以前將/consumer去掉。同時將spring.cloud.gateway.discovery.locator.enabled改成false,若是不改的話,以前的http://localhost:8081/service-consumer/user/info這樣的請求地址也能正常訪問,由於這時爲每一個服務建立了2個router。

本文第二部分和本節一共講述了兩種配置方式,兩種配置均可以實現請求路由轉發的功能。參數spring.cloud.gateway.discovery.locator.enabled爲true,代表Gateway開啓服務註冊和發現的功能,而且Spring Cloud Gateway自動根據服務發現爲每個服務建立了一個router,這個router將以服務名開頭的請求路徑轉發到對應的服務。spring.cloud.gateway.discovery.locator.lowerCaseServiceId是將請求路徑上的服務名配置爲小寫(由於服務註冊的時候,向註冊中心註冊時將服務名轉成大寫的了)。

gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true

02

Gateway跨域訪問

Spring Cloud Gateway還針對跨域訪問作了設計,可使用如下配置解決跨域訪問問題:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "https://docs.spring.io"
            allowedMethods:
            - GET
            allowHeaders:
            - Content-Type

在上面的示例中,容許來自https://docs.spring.io的get請...

03

Gateway 過濾器

Spring Cloud Gateway的filter生命週期不像Zuul那麼豐富,它只有兩個:「pre」和「post」:

  • pre:這種過濾器在請求被路由以前調用。能夠利用這個過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試的信息。
  • post:這種過濾器在路由到服務器以後執行。這種過濾器可用來爲響應添加HTTP Header、統計信息和指標、響應從微服務發送給客戶端等。

Spring Cloud gateway的filter分爲兩種:GatewayFilter和Globalfilter。GlobalFilter會應用到全部的路由上,而Gatewayfilter將應用到單個路由或者一個分組的路由上。

利用Gatewayfilter能夠修改請求的http的請求或者是響應,或者根據請求或者響應作一些特殊的限制。更多時候能夠利用Gatewayfilter作一些具體的路由配置。

下面的配置是AddRequestParameter Gatewayfilter的相關配置。

spring:
  application:
    name: service-gateway
  cloud:
    gateway:
     discovery:
        locator:
         enabled: true
     routes:
     - id: parameter_route
      uri: http://localhost:8504/user/info
      filters:
      - AddRequestParameter=foo, bar
      predicates:
      - Method=GET

上述配置中指定了轉發的地址,設置全部的GET方法都會自動添加foo=bar,當請求符合上述路由條件時,便可在後端服務上接收到Gateway網關添加的參數。

另外再介紹一種比較經常使用的filter,即StripPrefix gateway filter。

配置以下:

spring:
  cloud:
    gateway:
      routes:
      - id: stripprefixfilter
        uri: lb://service-consumer
        predicates:
        - Path=/consumer/**
        filters:
        - StripPrefix=1

當client端使用http://localhost:8098/consumer/user/info路徑進行請求時,若是根據上述進行配置Gateway會將請求轉換爲http://localhost:8098/service-consumer/user/info。以此做爲前端請求的最終目的地。

04

Gateway請求匹配

Gateway網關能夠根據不一樣的方式進行匹配進而把請求分發到不一樣的後端服務上。

經過header進行匹配,把請求分發到不一樣的服務上,配置以下:

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: http://baidu.com
        predicates:
        - Header=X-Request-Id, \d+

經過curl測試:curl http://localhost:8080 -H "X-Request-Id:666666",返回頁面代碼證實匹配成功。

若是是以Host進行匹配,配置以下:

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: http://baidu.com
        predicates:
        - Host=**.baidu.com

經過curl http://localhost:8098 -H "Host: www.baidu.com"進行測試,返回頁面代碼即轉發成功。

能夠經過POST、GET、PUT、DELTE等不一樣的方式進行路由:

spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: http://baidu.com
        predicates:
        - Method=GET

經過 curl http://localhost:8098 進行測試,返回頁面代碼即表示成功。

上述是單個匹配進行路由,若是把多個匹配合在一塊兒進行路由,必須知足全部的路有條件纔會進行路由轉發。

05

Gateway熔斷

Spring Cloud Gateway也能夠利用Hystrix的熔斷特性,在流量過大時進行服務降級,同時項目中必須加上Hystrix的依賴。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

配置後,Gateway將使用fallbackcmd做爲名稱生成HystrixCommand對象進行熔斷處理。若是想添加熔斷後的回調內容,須要添加如下配置:

spring:
  cloud:
    gateway:
      routes:
      - id: hystrix_route
        uri: lb://consumer-service
        predicates:
        - Path=/consumer/**
        filters:
        - name: Hystrix
          args:
            name: fallbackcmd
            fallbackUri: forward:/fallback
        - StripPrefix=1
hystrix:  
  command:
    fallbackcmd:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000 #超時時間,若不設置超時時間則有可能沒法觸發熔斷

上述配置中給出了熔斷以後返回路徑,所以,在Gateway服務模塊添加/fallback路徑,以做爲服務熔斷時的返回路徑。

@RestController
public class GatewayController {
    @RequestMapping(value = "/fallback")
    public String fallback(){
        return "fallback nothing";
    }
}

fallbackUri: forward:/fallback配置了 fallback 時要會調的路徑,當調用 Hystrix 的 fallback 被調用時,請求將轉發到/fallback這個 URI,並以此路徑的返回值做爲返回結果。

06

Gateway重試路由器

經過簡單的配置,Spring Cloud Gateway就能夠支持請求重試功能。

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: http://localhost:8504/user/info
        predicates:
        - Path=/user/**
        filters:
        - name: Retry
          args:
            retries: 3
            status: 503
        - StripPrefix=1

Retry GatewayFilter經過四個參數來控制重試機制,參數說明以下:

  • retries:重試次數,默認值是 3 次。
  • statuses:HTTP 的狀態返回碼,取值請參考:org.springframework.http.HttpStatus。
  • methods:指定哪些方法的請求須要進行重試邏輯,默認值是 GET 方法,取值參考:org.springframework.http.HttpMethod。
  • series:一些列的狀態碼配置,取值參考:org.springframework.http.HttpStatus.Series。符合的某段狀態碼纔會進行重試邏輯,默認值是 SERVER_ERROR,值是 5,也就是 5XX(5 開頭的狀態碼),共有5個值。

使用上述配置進行測試,當後臺服務不可用時,會在控制檯看到請求三次的日誌,證實此配置有效。

07

Gateway 限流操做

Spring Cloud Gateway自己集成了限流操做,Gateway限流須要使用Redis,pom文件中添加Redis依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

配置文件中配置以下:

spring:
  cloud:
    gateway:
      routes:
      - id: rate_limit_route
        uri: lb://service-consumer
        predicates:
        - Path=/user/**
        filters:
        - name: RequestRateLimiter
          args:
            key-resolver: "#{@hostAddrKeyResolver}"
            redis-rate-limiter.replenishRate: 1
            redis-rate-limiter.burstCapacity: 3
        - StripPrefix=1
    consul:
      host: 127.0.0.1
      port: 8500
      discovery:
        service-name: service-gateway
        instance-id: service-gateway-233

  redis:
    host: localhost
    port: 6379

在上面的配置問價中,配置了Redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器須要配置三個參數:

  • BurstCapacity:令牌桶的總容量。
  • replenishRate:令牌通每秒填充平均速率。
  • Key-resolver:用於限流的解析器的Bean對象的名字。它使用SpEL表達式#{@beanName}從Spring容器中獲取bean對象。

注意:filter下的name必須是RequestRateLimiter。

Key-resolver參數後面的bean須要本身實現,而後注入到Spring容器中。KeyResolver須要實現resolve方法,好比根據ip進行限流,則須要用hostAddress去判斷。實現完KeyResolver以後,須要將這個類的Bean註冊到Ioc容器中。還能夠根據uri限流,同hostname限流是同樣的。例如以ip限流爲例,在gateway模塊中添加如下實現:

public class HostAddrKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

    public HostAddrKeyResolver hostAddrKeyResolver() {
        return new HostAddrKeyResolver();
    }
}

把該類注入到spring容器中:

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean
    public HostAddrKeyResolver hostAddrKeyResolver(){
        return new HostAddrKeyResolver();
    }
}

基於上述配置,能夠對請求基於ip的訪問進行限流。

08

自定義Gatewayfilter

Spring Cloud Gateway內置了過濾器,可以知足不少場景的需求。固然,也能夠自定義過濾器。在Spring Cloud Gateway自定義過濾器,過濾器須要實現GatewayFilter和Ordered這兩個接口。

下面的例子實現了Gatewayfilter,它能夠以log日誌的形式記錄每次請求耗費的時間,具體實現以下:

public class RequestTimeFilter implements GatewayFilter, Ordered {
    private static final Log log = LogFactory.getLog(GatewayFilter.class);
    private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
                    if (startTime != null) {
                        log.info("請求路徑:"+exchange.getRequest().getURI().getRawPath() + "消耗時間: " + (System.currentTimeMillis() - startTime) + "ms");
                    }
                })
        );
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

上述代碼中定義了本身實現的過濾器。Ordered的int getOrder()方法是來給過濾器定優先級的,值越大優先級越低。還有一個filter(ServerWebExchange exchange, GatewayFilterChain chain)方法,在該方法中,先記錄了請求的開始時間,並保存在ServerWebExchange中,此處是一個「pre」類型的過濾器。而後再chain.filter()的內部類中的run()方法中至關於"post"過濾器,在此處打印了請求所消耗的時間。

接下來將該過濾器註冊到router中,代碼以下。

@Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/user/**")
                        .filters(f -> f.filter(new RequestTimeFilter())
                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                        .uri("http://localhost:8504/user/info")
                        .order(0)
                        .id("customer_filter_router")
                )
                .build();
    }

除了上述代碼的方式配置咱們自定義的過濾器的方式以外,也能夠在application.yml文件中直接配置,這裏再也不贅述。

啓動程序,經過curl http://localhost:8098/user/info控制檯會打印出請求消耗時間,日誌以下:

....
2019-05-22 15:13:31.221  INFO 19780 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter   : 請求路徑:/user/info消耗時間: 54ms
...
2019-05-22 16:46:23.785  INFO 29928 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter   : 請求路徑:/user/info3消耗時間: 5ms
....

09

自定義GlobalFilter

Spring Cloud Gateway根據做用範圍分爲GatewayFilter和GlobalFilter,兩者區別以下:

GatewayFilter : 須要經過spring.cloud.routes.filters 配置在具體路由下,只做用在當前路由上或經過spring.cloud.default-filters配置在全局,做用在全部路由上。

GlobalFilter:全局過濾器,不須要在配置文件中配置,做用在全部的路由上,最終經過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器,它爲請求業務以及路由的URI轉換爲真實業務服務的請求地址的核心過濾器,不須要配置,系統初始化時加載,並做用在每一個路由上。

在上一小節中定義的是Gatewayfilter,下面實現的是Globalfilter:

public class TokenFilter implements GlobalFilter, Ordered {

Logger logger= LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    String token = exchange.getRequest().getQueryParams().getFirst("token");
    if (token == null || token.isEmpty()) {
        logger.info( "token 爲空,沒法進行訪問." );
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }
    return chain.filter(exchange);
}

@Override
public int getOrder() {
    return 0;
}

}

上述代碼實現了Globalfilter,具體邏輯是判斷請求中是否含參數token,若是沒有,則校驗不經過,對全部請求都有效。若是含有token則轉發到具體後端服務上,若是沒有則校驗不經過。

經過curl http://localhost:8098/user/info進行訪問,由於路徑中不含有參數token,則沒法經過校驗,打印日誌以下:

2019-05-22 15:27:11.078  INFO 5956 --- [ctor-http-nio-1] com.song.gateway.TokenFilter             : token 爲空,沒法進行訪問.
...

經過curl http://localhost:8098/user/info?token=123進行訪問時,則能夠獲取到後端服務返回結果。

相關文章
相關標籤/搜索