在上一篇博客中,博主介绍了
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
(客户端需要再次请求,和请求转发不一样)。