在上一篇部落格中,部落客介紹了
Gateway
以及它的路由斷言工廠:
- Spring Cloud Alibaba:Gateway網關 & 路由斷言工廠
目前,
Gateway
提供了三十多種路由過濾器工廠,部落客打算用幾篇部落格來介紹一些常用的路由過濾器工廠。路由過濾器允許以某種方式修改傳入的
HTTP
請求或傳出的
HTTP
響應。路由過濾器的範圍是特定的路由(請求需要比對路由,即比對路由的斷言集合,路由過濾器鍊才會産生作用)。
Spring Cloud 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
的服務清單就會出現這兩個服務。
AddRequestHeader
AddRequestHeader
路由過濾器工廠接受兩個參數,請求頭名稱和值。将為所有比對的請求向下遊請求的
Header
中添加設定的請求頭名稱和值。
源碼相關部分:
添加路由過濾器的配置(類似路由斷言配置):
filters:
- AddRequestHeader=Gateway-AddRequestHeader-kaven, itkaven
使用
Postman
進行測試,結果符合預期。
AddRequestParameter
AddRequestParameter
路由過濾器工廠接受兩個參數,參數名稱和值。将為所有比對的請求向下遊請求的參數中添加設定的參數名稱和值。
源碼相關部分:
修改路由過濾器的配置:
filters:
- AddRequestParameter=Gateway-AddRequestParameter-kaven, itkaven
AddResponseHeader
AddResponseHeader
路由過濾器工廠接受兩個參數,響應頭名稱和值。将為所有比對的請求向下遊響應的
Header
中添加設定的響應頭名稱和值。
源碼相關部分:
修改路由過濾器的配置:
filters:
- AddResponseHeader=Gateway-AddResponseHeader-kaven, itkaven
PrefixPath
PrefixPath
路由過濾器工廠接受單個
prefix
參數,會将
prefix
作為所有比對請求路徑的字首。
源碼相關部分:
修改路由過濾器的配置:
predicates:
- Path=/**
filters:
- PrefixPath=/message
請求
http://127.0.0.1:8085
,會被路由到
http://localhost:8080/message
。
為了示範,在
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
路由過濾器有多個時,隻有第一個起作用。
RequestRateLimiter
RequestRateLimiter
路由過濾器工廠使用
RateLimiter
實作來确定是否繼續處理目前請求。如果不處理,則響應
429 Too Many Requests
。此過濾器接受一個可選
keyResolver
參數(該參數用于指定限速的對象,如
URL
、使用者
ID
等)和特定于
RateLimiter
實作的參數(令牌桶填充速率和容量)。
keyResolver
是一個實作
KeyResolver
接口的
bean
。在配置中,使用
SpEL
按名稱引用
bean
。
#{@ipKeyResolver}
是一個
SpEL
表達式,引用一個名為
ipKeyResolver
的
bean
。
源碼相關部分:
内部使用
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
Redis
中存儲的資訊:
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
的值。
源碼相關部分:
當用戶端接收到需要重定向的響應時(狀态碼
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
請求
http://127.0.0.1:8085/message
,被重定向到
http://localhost:8085/redirect
(用戶端需要再次請求,和請求轉發不一樣)。