In the last blog, the blogger introduced Gateway and its routing assertion factory:
At present, Gateway has 24 kinds of routing filter factories. Bloggers intend to introduce these routing filter factories with several blogs. Routing filters allow you to modify incoming HTTP requests or outgoing HTTP responses in some way. The scope of the routing filter is a specific route (the request needs to match the route, that is, the assertion set of the matching route, and the routing filter will work).
Working mode of Spring Cloud Gateway (figure from official website):
The client sends a request to the Spring Cloud Gateway. If the Gateway Handler Mapping determines that the request matches the route, it sends it to the Gateway Web Handler. This handler converts requests into proxy requests through a chain of request specific filters. The reason why filters are separated by dashed lines is that filters may execute logic before or after sending proxy requests. Execute all pre filter logic (acting on the request), and then issue the proxy request. After the proxy request receives the response, execute all post filter logic (acting on the response).
Construction works
One parent module and two child modules (the nacos module provides services, and the gateway module implements the gateway).
POM of parent module 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
Interface definition:
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(); } // Get the StringBuilder composed of key and value in header and parameter 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; } }
Startup class:
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
Startup class:
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); } }
Start these two module s, and these two services will appear in the service list of Nacos.
AddRequestHeader
The AddRequestHeader routing filter factory accepts two parameters, the request Header name and the value. The set request Header name and value will be added to the Header of downstream requests for all matching requests.
Relevant parts of source code:
Add the configuration of routing filter (similar to routing assertion configuration):
filters: - AddRequestHeader=Gateway-AddRequestHeader-kaven, itkaven
Postman was used to test, and the results were in line with expectations.
AddRequestParameter
AddRequestParameter the routing filter worker accepts two parameters, parameter name and value. The set parameter name and value will be added to the parameters of downstream requests for all matching requests.
Relevant parts of source code:
Modify the configuration of routing filter:
filters: - AddRequestParameter=Gateway-AddRequestParameter-kaven, itkaven
AddResponseHeader
The AddResponseHeader routing filter factory accepts two parameters, the response Header name and the value. The set response Header name and value will be added to the Header of the downstream response for all matching requests.
Relevant parts of source code:
Modify the configuration of routing filter:
filters: - AddResponseHeader=Gateway-AddResponseHeader-kaven, itkaven
PrefixPath
The PrefixPath routing filter factory accepts a single prefix parameter and will prefix all matching request paths.
Relevant parts of source code:
Modify the configuration of routing filter:
predicates: - Path=/** filters: - PrefixPath=/message
request http://127.0.0.1:8085 , will be routed to http://localhost:8080/message .
For demonstration, add an interface in the nacos module:
@GetMapping("/message/prefix") public String prefix() { return "PrefixPath"; }
Modify the configuration of routing filter:
filters: - PrefixPath=/message - PrefixPath=/kaven
request http://127.0.0.1:8085/prefix , will be routed to http://localhost:8080/message/prefix , when there are multiple PrefixPath routing filters, only the first one works.
RequestRateLimiter
The RequestRateLimiter routing filter factory uses the RateLimiter implementation to determine whether to continue processing the current request. If not, 429 Too Many Requests will be responded. This filter takes an optional keyresolver parameter and a rate limiter specific parameter. Keyresolver is a bean that implements the keyresolver interface. In the configuration, use spiel to reference beans by name# {@ ipKeyResolver} is a spiel expression that references a bean named ipKeyResolver.
Relevant parts of source code:
Redis+Lua is used internally to limit current. The flow restriction rules are determined by the specific implementation class of KeyResolver interface, such as flow restriction through IP, URL, etc. Due to the use of redis, it is necessary to increase the dependence on redis:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
Define a bean in the startup class (specific implementation of KeyResolver interface):
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 current limiting @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()); } }
Modify the configuration of routing filter:
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: token bucket fill rate, in seconds.
- redis-rate-limiter.burstCapacity: token bucket capacity (setting this value to zero will block all requests).
- Key resolver: use spiel to reference bean s by name.
Lua script:
--Key for number of tokens local tokens_key = KEYS[1] --Timestamp key local timestamp_key = KEYS[2] --redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key) --Token bucket fill rate local rate = tonumber(ARGV[1]) --Token bucket capacity local capacity = tonumber(ARGV[2]) --Current timestamp local now = tonumber(ARGV[3]) --Number of tokens requested local requested = tonumber(ARGV[4]) --Time required for token bucket to fill local fill_time = capacity/rate --ttl Twice as much fill_time Round down again 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) --How many tokens were left last time local last_tokens = tonumber(redis.call("get", tokens_key)) --If there is no record, the token bucket is full if last_tokens == nil then last_tokens = capacity end --redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens) --Timestamp of last update local last_refreshed = tonumber(redis.call("get", timestamp_key)) --If there is no record, it is 0 if last_refreshed == nil then last_refreshed = 0 end --redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed) --The time stamp difference between the token obtained this time and the token obtained last time local delta = math.max(0, now-last_refreshed) --according to delta Calculate the current number of tokens, which cannot exceed the token bucket capacity local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) --Whether the request token is allowed, that is, the number of current tokens should not be less than the number of requested tokens local allowed = filled_tokens >= requested -- local new_tokens = filled_tokens local allowed_num = 0 --If the token request is allowed, the current number of tokens will be updated 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) --to update redis Number of timestamps and tokens in 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 }
Therefore, the number of tokens and timestamp are stored in Redis. As can also be seen from the script, You can allow requests for temporary bursts by setting burstCapacity higher than replenishRate (burst) because the number of tokens is initially the value set by burstCapacity, temporary bursts are allowed to be requested at this time. Because the filling speed of the token bucket is less than the capacity of the token bucket, the number of tokens does not allow continuous request bursts after requesting temporary bursts, and it is necessary to wait until the number of tokens increases to an appropriate number.
RedirectTo
The RedirectTo routing filter factory accepts two parameters, status status and redirect address url. The status should be a 300 series redirect HTTP status code, such as 301. The url should be a valid redirect address, which will be the value of the Location Header in the Gateway response.
Relevant parts of source code:
When the client receives a response requiring redirection (the status code 300 series indicates redirection), it will request the redirection address set by the Location Header in the response.
For demonstration, add an interface in the 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"; } }
Modify the configuration of routing filter:
filters: - RedirectTo=301, http://localhost:8085/redirect
Other routing filter factories will be introduced in future blogs. If the blogger has something wrong or you have different opinions, you are welcome to comment and supplement.