天天看點

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

在上一篇部落格中,部落客介紹了​

​Gateway​

​以及它的路由斷言工廠:

  • ​​Spring Cloud Alibaba:Gateway網關 & 路由斷言工廠​​

目前,​

​Gateway​

​​提供了三十多種路由過濾器工廠,部落客打算用幾篇部落格來介紹一些常用的路由過濾器工廠。路由過濾器允許以某種方式修改傳入的​

​HTTP​

​​請求或傳出的​

​HTTP​

​響應。路由過濾器的範圍是特定的路由(請求需要比對路由,即比對路由的斷言集合,路由過濾器鍊才會産生作用)。

​Spring Cloud Gateway​

​工作方式(圖來自官網):

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

用戶端向​

​Spring Cloud Gateway​

​​送出請求。如果​

​Gateway Handler Mapping​

​​确定請求與路由比對,則将其發送到​

​Gateway Web Handler​

​​。此處理程式通過特定于請求的過濾器鍊,将請求轉換成代理請求。過濾器被虛線分隔的原因是過濾器可能在發送代理請求之前或之後執行邏輯。執行所有​

​pre​

​​過濾器邏輯(作用于請求),然後發出代理請求。代理請求得到響應後,執行所有​

​post​

​過濾器邏輯(作用于響應)。

搭建工程

一個父​

​module​

​​和兩個子​

​module​

​​(​

​nacos module​

​​提供服務,​

​gateway module​

​實作網關)。

父​

​module​

​​的​

​pom.xml​

​:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kaven</groupId>
    <artifactId>alibaba</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <description>Spring Cloud Alibaba</description>
    <modules>
        <module>nacos</module>
        <module>gateway</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-cloud-version>Hoxton.SR9</spring-cloud-version>
        <spring-cloud-alibaba-version>2.2.6.RELEASE</spring-cloud-alibaba-version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <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>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>      

nacos module

​pom.xml​

​:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>com.kaven</groupId>
        <artifactId>alibaba</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>nacos</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>      

​application.yml​

​:

server:
  port: 8080

spring:
  application:
    name: nacos
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.197:9000      

接口定義:

package com.kaven.alibaba.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@RestController
public class MessageController {

    @GetMapping("/message")
    public String getMessage(HttpServletRequest httpServletRequest) {
        StringBuilder message = new StringBuilder("hello kaven, this is nacos\n");
        message.append(getKeyAndValue(httpServletRequest));
        return message.toString();
    }

    // 擷取header和parameter中key和value組成的StringBuilder
    private StringBuilder getKeyAndValue(HttpServletRequest httpServletRequest) {
        StringBuilder result = new StringBuilder();
        Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            String value = httpServletRequest.getHeader(key);
            result.append(key).append(" : ").append(value).append("\n");
        }
        Enumeration<String> parameterNames = httpServletRequest.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String key = parameterNames.nextElement();
            String value = httpServletRequest.getParameter(key);
            result.append(key).append(" : ").append(value).append("\n");
        }
        return result;
    }
}      

啟動類:

package com.kaven.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

gateway module

​pom.xml​

​:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>com.kaven</groupId>
        <artifactId>alibaba</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>gateway</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>
</project>      

​application.yml​

​:

server:
  port: 8085

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.1.197:9000
    gateway:
      routes:
        - id: nacos
          uri: http://localhost:8080
          predicates:
            - Path=/message      

啟動類:

package com.kaven.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

啟動這兩個​

​module​

​​,​

​Nacos​

​的服務清單就會出現這兩個服務。

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

AddRequestHeader

​AddRequestHeader​

​​路由過濾器工廠接受兩個參數,請求頭名稱和值。将為所有比對的請求向下遊請求的​

​Header​

​中添加設定的請求頭名稱和值。

源碼相關部分:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

添加路由過濾器的配置(類似路由斷言配置):

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)
filters:
            - AddRequestHeader=Gateway-AddRequestHeader-kaven, itkaven      

使用​

​Postman​

​進行測試,結果符合預期。

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

AddRequestParameter

​AddRequestParameter​

​路由過濾器工廠接受兩個參數,參數名稱和值。将為所有比對的請求向下遊請求的參數中添加設定的參數名稱和值。

源碼相關部分:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

修改路由過濾器的配置:

filters:
            - AddRequestParameter=Gateway-AddRequestParameter-kaven, itkaven      
Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

AddResponseHeader

​AddResponseHeader​

​​路由過濾器工廠接受兩個參數,響應頭名稱和值。将為所有比對的請求向下遊響應的​

​Header​

​中添加設定的響應頭名稱和值。

源碼相關部分:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

修改路由過濾器的配置:

filters:
            - AddResponseHeader=Gateway-AddResponseHeader-kaven, itkaven      
Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

PrefixPath

​PrefixPath​

​​路由過濾器工廠接受單個​

​prefix​

​​參數,會将​

​prefix​

​作為所有比對請求路徑的字首。

源碼相關部分:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

修改路由過濾器的配置:

predicates:
            - Path=/**
          filters:
            - PrefixPath=/message      

請求​

​http://127.0.0.1:8085​

​​,會被路由到​

​http://localhost:8080/message​

​。

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

為了示範,在​

​nacos module​

​中添加一個接口:

@GetMapping("/message/prefix")
    public String prefix() {
        return "PrefixPath";
    }      

修改路由過濾器的配置:

filters:
            - PrefixPath=/message
            - PrefixPath=/kaven      

請求​

​http://127.0.0.1:8085/prefix​

​​,會被路由到​

​http://localhost:8080/message/prefix​

​​,是以,當​

​PrefixPath​

​路由過濾器有多個時,隻有第一個起作用。

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

RequestRateLimiter

​RequestRateLimiter​

​​路由過濾器工廠使用​

​RateLimiter​

​​實作來确定是否繼續處理目前請求。如果不處理,則響應​

​429 Too Many Requests​

​​。此過濾器接受一個可選​

​keyResolver​

​​參數(該參數用于指定限速的對象,如​

​URL​

​​、使用者​

​ID​

​​等)和特定于​

​RateLimiter​

​​實作的參數(令牌桶填充速率和容量)。​

​keyResolver​

​​是一個實作​

​KeyResolver​

​​接口的​

​bean​

​​ 。在配置中,使用​

​SpEL​

​​按名稱引用​

​bean​

​​。​

​#{@ipKeyResolver}​

​​是一個​

​SpEL​

​​表達式,引用一個名為​

​ipKeyResolver​

​​的​

​bean​

​。

源碼相關部分:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)
Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

内部使用​

​Redis+Lua​

​​實作限流。限流規則由​

​KeyResolver​

​​接口的具體實作類來決定,比如通過​

​IP​

​​、​

​URL​

​​等來進行限流。由于用到​

​Redis​

​​,需要增加​

​Redis​

​的依賴:

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

在啟動類中定義一個​

​bean​

​​(​

​KeyResolver​

​接口的具體實作):

package com.kaven.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Mono;

import java.util.Objects;

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

    // IP限流
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName());
    }
}      

修改路由過濾器的配置:

predicates:
            - Path=/message
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 2
                key-resolver: "#{@ipKeyResolver}"
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379      
  • ​redis-rate-limiter.replenishRate​

    ​:令牌桶填充速率,機關為秒。
  • ​redis-rate-limiter.burstCapacity​

    ​:令牌桶容量(将此值設定為零将阻止所有請求)。
  • ​key-resolver​

    ​​:使用​

    ​SpEL​

    ​​按名稱引用​

    ​bean​

    ​。
Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

​Redis​

​中存儲的資訊:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

​Lua​

​腳本:

--令牌數的鍵
local tokens_key = KEYS[1]
--時間戳的鍵
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

--令牌桶填充速率
local rate = tonumber(ARGV[1])
--令牌桶容量
local capacity = tonumber(ARGV[2])
--現在的時間戳
local now = tonumber(ARGV[3])
--請求的令牌數量
local requested = tonumber(ARGV[4])

--令牌桶填滿需要的時間
local fill_time = capacity/rate
--ttl為兩倍fill_time再向下取整
local ttl = math.floor(fill_time*2)

--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)

--上一次還剩多少令牌
local last_tokens = tonumber(redis.call("get", tokens_key))
--如果沒有記錄(沒有使用過令牌,或者使用令牌的時間間隔超過ttl),相當于令牌桶已經滿了
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

--上一次更新的時間戳
local last_refreshed = tonumber(redis.call("get", timestamp_key))
--如果沒有記錄,為0
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

--此次擷取令牌與上次擷取令牌的時間戳內插補點
local delta = math.max(0, now-last_refreshed)
--根據delta計算目前的令牌數,不能超過令牌桶容量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
--此次請求令牌是否允許,即目前令牌數要不小于請求的令牌數
local allowed = filled_tokens >= requested
--
local new_tokens = filled_tokens
local allowed_num = 0
--如果允許此次請求令牌,則更新目前令牌數
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)

--更新redis中的時間戳和令牌數
if ttl > 0 then
  redis.call("setex", tokens_key, ttl, new_tokens)
  redis.call("setex", timestamp_key, ttl, now)
end

-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }      

是以​

​Redis​

​​中存儲的就是令牌數和時間戳。由該腳本也可知,可以通過設定​

​burstCapacity​

​​高于​

​replenishRate​

​​來允許請求臨時突發(​

​burst​

​​),因為令牌數初始為​

​burstCapacity​

​設定的值,此時允許請求臨時突發,因為令牌桶填充速率小于令牌桶容量,是以在請求臨時突發後,令牌數就不允許持續的請求突發了,需要再等令牌數填充到合适的數量才行。

RedirectTo

​RedirectTo​

​​路由過濾器工廠接受兩個參數,狀态​

​status​

​​和重定向位址​

​url​

​​參數。​

​status​

​​是一個​

​300​

​​系列的重定向​

​HTTP​

​​狀态碼,比如​

​301​

​​。​

​url​

​​應該是一個有效的重定向位址,這将是​

​Gateway​

​​響應中​

​Location Header​

​的值。

源碼相關部分:

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

當用戶端接收到需要重定向的響應時(狀态碼​

​300​

​​系清單示重定向),就會去請求響應中​

​Location Header​

​設定的重定向位址。

為了示範,在​

​gateway module​

​中增加一個接口:

package com.kaven.alibaba.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RedirectToController {
    @GetMapping("/redirect")
    public String redirect() {
        return "redirect";
    }
}      

修改路由過濾器的配置:

filters:
            - RedirectTo=301, http://localhost:8085/redirect      
Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)

請求​

​http://127.0.0.1:8085/message​

​​,被重定向到​

​http://localhost:8085/redirect​

​(用戶端需要再次請求,和請求轉發不一樣)。

Spring Cloud Alibaba:Gateway之路由過濾器工廠(一)