0
点赞
收藏
分享

微信扫一扫

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)


在上一篇博客中,博主介绍了​​Gateway​​以及它的路由断言工厂:

  • ​​Spring Cloud Alibaba:Gateway网关 & 路由断言工厂​​

目前,​​Gateway​​​提供了三十多种路由过滤器工厂,博主打算用几篇博客来介绍一些常用的路由过滤器工厂。路由过滤器允许以某种方式修改传入的​​HTTP​​​请求或传出的​​HTTP​​响应。路由过滤器的范围是特定的路由(请求需要匹配路由,即匹配路由的断言集合,路由过滤器链才会产生作用)。

​Spring Cloud Gateway​​工作方式(图来自官网):

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_java


客户端向​​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之路由过滤器工厂(一)_网络_02

AddRequestHeader

​AddRequestHeader​​​路由过滤器工厂接受两个参数,请求头名称和值。将为所有匹配的请求向下游请求的​​Header​​中添加设置的请求头名称和值。

源码相关部分:

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_spring_03


添加路由过滤器的配置(类似路由断言配置):

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_maven_04

filters:
- AddRequestHeader=Gateway-AddRequestHeader-kaven, itkaven

使用​​Postman​​进行测试,结果符合预期。

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_maven_05

AddRequestParameter

​AddRequestParameter​​路由过滤器工厂接受两个参数,参数名称和值。将为所有匹配的请求向下游请求的参数中添加设置的参数名称和值。

源码相关部分:

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_网络_06


修改路由过滤器的配置:

filters:
- AddRequestParameter=Gateway-AddRequestParameter-kaven, itkaven

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_java_07

AddResponseHeader

​AddResponseHeader​​​路由过滤器工厂接受两个参数,响应头名称和值。将为所有匹配的请求向下游响应的​​Header​​中添加设置的响应头名称和值。

源码相关部分:

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_gateway_08


修改路由过滤器的配置:

filters:
- AddResponseHeader=Gateway-AddResponseHeader-kaven, itkaven

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_maven_09

PrefixPath

​PrefixPath​​​路由过滤器工厂接受单个​​prefix​​​参数,会将​​prefix​​作为所有匹配请求路径的前缀。

源码相关部分:

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_java_10


修改路由过滤器的配置:

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

请求​​http://127.0.0.1:8085​​​,会被路由到​​http://localhost:8080/message​​。

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_java_11


为了演示,在​​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之路由过滤器工厂(一)_spring_12

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_13


Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_java_14


内部使用​​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之路由过滤器工厂(一)_网络_15


​Redis​​中存储的信息:

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_java_16


​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之路由过滤器工厂(一)_spring_17

当客户端接收到需要重定向的响应时(状态码​​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之路由过滤器工厂(一)_网络_18


请求​​http://127.0.0.1:8085/message​​​,被重定向到​​http://localhost:8085/redirect​​(客户端需要再次请求,和请求转发不一样)。

Spring Cloud Alibaba:Gateway之路由过滤器工厂(一)_网络_19


其他的路由过滤器工厂在以后的博客中介绍,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。


举报

相关推荐

0 条评论