在上一篇博客中,博主介绍了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
(客户端需要再次请求,和请求转发不一样)。
其他的路由过滤器工厂在以后的博客中介绍,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。