Spring Cloud Alibaba: Gateway's routing filter factory

Posted by senyo on Tue, 21 Dec 2021 06:21:11 +0100

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.

Topics: Java network Spring Cloud gateway