"springcloud 2021 series" gateway new generation API gateway service

After the Netflix open source abortion, Spring is constantly replacing Netflix related components, such as Eureka, Zuul, Feign, Ribbon, etc. Zuul's alternative product is SpringCloud Gateway, which is a gateway component developed by the Spring team, which can realize new features such as security authentication, current limiting, Retry, and support for long connections.

Background description

If there are three services, i.e. product service and account service. Now there are client WEB applications or APP applications that need to access back-end services to obtain data, so the access paths of the three services need to be maintained on the client.

Such an architecture will have the following typical problems:

  • Each microservice needs to open external network access rights and configure a separate access domain name. For each new service, the operation and maintenance personnel need to configure the domain name mapping first
  • The client needs to maintain the access addresses of all microservices. Imagine if there are hundreds of microservices
  • When the service needs to control the permission of the interface, and the user must be authenticated before calling, then all the permission logic must be rewritten on the server side
  • . . .

Therefore, a gateway service needs to be added before the micro service, so that all clients can access the gateway and the gateway is responsible for forwarding requests; Put the permission verification logic into the filter of the gateway, and the back-end service no longer needs to pay attention to the code of permission verification; You only need to provide a domain name address that can be accessed by the external network. After the new service is added, you don't need to ask the operation and maintenance personnel to configure the network. In this way, the above architecture is as follows:

GateWay introduction

In the spring cloud architecture, a separate gateway service needs to be deployed to provide external access, and then the gateway service forwards the request to the specific back-end service according to the configured rules.

Spring Cloud Gateway is a new sub project of spring cloud, which is based on spring 5 x,SpringBoot2.x technology version is written to provide a simple, convenient and scalable unified API routing management method.

Basic concepts

  • Route

    Routing is the basic unit of gateway, which is composed of ID, URI, a group of Predicate and a group of Filter. It is matched and forwarded according to the Predicate

  • Predicate (assertion)

    It refers to the Function Predicate of Java 8, and the input type is ServerWebExchange in the Spring framework. As the judgment condition of routing and forwarding, Spring cloud gateway currently supports a variety of methods, such as Path, Query, Method, Header, etc

  • Filter

    The filter is the filtering logic that passes through when routing and forwarding requests. The instance of GatewayFilter can be used to modify the contents of requests and responses

Workflow

The client sends a request to the Spring Cloud Gateway. If the gateway handler mapping determines that the request matches the route, it is sent to the gateway Web handler. This handler sends requests through a request specific filtering chain at run time. The reason why filters are separated by dashed lines is that filters can execute logic before or after sending proxy requests.

Predicates assertion

When this condition is met, it will be forwarded. If there are multiple, it will be forwarded when all conditions are met.

Matching modeexplainSample
BeforeBefore a certain point in timeBefore=2019-05-01T00:00:00+08:00[Asia/Shanghai]
AfterAfter a certain point in timeAfter=2019-04-29T00:00:00+08:00[Asia/Shanghai]
BetweenBefore +AfterBetween=2019-04-29T00:00:00+08:00[Asia/Shanghai], 2019-05-01T00:00:00+08:00[Asia/Shanghai]
CookieCookie valueCookie=hacfin, langyastudio
HeaderHeader valueHeader=X-Request-Id, \d+
Hosthost nameHost=**.langyastudio.com
MethodRequest modeMethod=POST
QueryRequest parametersQuery=xxx, zzz
PathRequest pathPath=/article/{articleId}
RemoteAddrRequest IPRemoteAddr=192.168.1.56/24
WeightweightWeight=group1, 8

Weight example:

80% of the requests will be routed to localhost:8201 and 20% to localhost:8202

spring:
  cloud:
    gateway:
      routes:
      - id: weight_high
        uri: http://localhost:8201
        predicates:
        - Weight=group1, 8
      - id: weight_low
        uri: http://localhost:8202
        predicates:
        - Weight=group1, 2

Filter filter

Routing filters can be used to modify incoming HTTP requests and returned HTTP responses. Spring Cloud Gateway has built-in multiple routing filters, which are generated by the factory class of GatewayFilter. The usage of common routing filters is described below.

AddRequestParameter add parameter

Filter to add parameters to the request

spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://localhost:8201
          filters:
            - AddRequestParameter=username, langyastudio
          predicates:
            - Method=GET

The above configuration will add the request parameter username=langyastudio to the GET request, and use the following command to test through curl tool

curl http://localhost:9201/user/getByUsername

Equivalent to initiating the request:

curl http://localhost:8201/user/getByUsername?username=langyastudio

StripPrefix prefix removal

A filter that removes a specified number of path prefixes

spring:
  cloud:
    gateway:
      routes:
      - id: strip_prefix_route
        uri: http://localhost:8201
        predicates:
        - Path=/user-service/**
        filters:
        - StripPrefix=2

The above configuration will remove two bits from the path of the request starting with / user service /, and use the following command to test through curl tool

curl http://localhost:9201/user-service/a/user/1

Equivalent to initiating the request:

curl http://localhost:8201/user/1

PrefixPath prefix increase

In contrast to the StripPrefix filter, the filter that adds operations to the original path prefix

spring:
  cloud:
    gateway:
      routes:
      - id: prefix_path_route
        uri: http://localhost:8201
        predicates:
        - Method=GET
        filters:
        - PrefixPath=/user

The above configuration will add / user path prefix to all GET requests, and use the following command to test through curl tool

curl http://localhost:9201/1

Equivalent to initiating the request:

curl http://localhost:8201/user/1

RequestRateLimiter current limiting

The RequestRateLimiter filter can be used to limit the flow. The RateLimiter implementation is used to determine whether to allow the current request to continue. If the request is too large, it will return HTTP 429 - too many request status by default.

In POM Add related dependencies to XML

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

Add the configuration class of current limiting policy. There are two kinds of policies. One is to limit the current according to the username in the request parameter, and the other is to limit the current according to the access IP

@Configuration
public class RedisRateLimiterConfig {
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("username"));
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}

Redis is used to limit the current, so the configuration of redis and RequestRateLimiter needs to be added. Here, all GET requests are limited by IP

server:
  port: 9201
spring:
  redis:
    host: localhost
    password: 123456
    port: 6379
  cloud:
    gateway:
      routes:
        - id: requestratelimiter_route
          uri: http://localhost:8201
          filters:
            - name: RequestRateLimiter
              args:
                #Number of requests allowed to be processed per second
                redis-rate-limiter.replenishRate: 1 
                #The capacity of the token bucket, the maximum number of requests allowed to be completed in one second
                redis-rate-limiter.burstCapacity: 2 
                #Current limiting policy, Bean corresponding to the policy
                #SpEL Expression basis#{@ beanName} get Bean object from Spring container
                key-resolver: "#{@ipKeyResolver}" 
          predicates:
            - Method=GET
logging:
  level:
    org.springframework.cloud.gateway: debug

Multiple requests for this address: http://localhost:9201/user/1 , an error with status code 429 will be returned

Retry retry

The filter that retries the routing request can determine whether to retry according to the HTTP status code returned by the routing request

To modify a profile:

spring:
  cloud:
    gateway:
      routes:
      - id: retry_route
        uri: http://localhost:8201
        predicates:
        - Method=GET
        filters:
        - name: Retry
          args:
            retries: 1 #Number of retries required
            statuses: BAD_GATEWAY #Which status code needs to be returned for retry? The status code returned is 5XX for retry
            backoff:
              firstBackoff: 10ms
              maxBackoff: 50ms
              factor: 2
              basedOnPreviousValue: false

When the call returns 500, it will retry and access the test address: http://localhost:9201/user/111

It can be found that the user service console reported an error twice, indicating a retry

2019-10-27 14:08:53.435 ERROR 2280 --- [nio-8201-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause

Custom filter

For example, after using the spring cloud architecture, we hope that all requests can be accessed only through the gateway. Without any processing, we can bypass the gateway and directly access the back-end services.

There are three main solutions to prevent bypassing the gateway and directly requesting back-end services:

  • Network isolation

    Common back-end services are deployed in the intranet, and only gateway applications are allowed to access back-end services through firewall policies

  • Application layer interception

    When requesting back-end services, check whether the request comes from the gateway through the interceptor. If it does not come from the gateway, it will prompt that access is not allowed

  • Deploy using Kubernetes

    When deploying the spring cloud architecture using Kubernetes, configure NodePort for the Service of the gateway, and use ClusterIp for the Service of other back-end services, so that only the gateway can be accessed outside the cluster

If application layer interception is adopted, add additional Header examples when the request passes through the gateway:

@Component
@Order(0)
public class GatewayRequestFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        byte[] token = Base64Utils.encode("GATEWAY_TOKEN_VALUE".getBytes());
        String[] headerValues = {new String(token)};        
        ServerHttpRequest build = exchange.getRequest()
                .mutate()
                .header("geteway_token", headerValues)
                .build();
        ServerWebExchange newExchange = exchange.mutate().request(build).build();
        
        return chain.filter(newExchange);
    }
}

GateWay actual combat

Source address: https://github.com/langyastudio/langya-tech/tree/master/spring-cloud

Use Nacos Discovery Starter and Spring Cloud Gateway Starter to complete the routing of Spring Cloud services.

  • Nacos Alibaba is an open source dynamic service discovery, configuration management and service management platform that is easier to build cloud native applications
  • Spring Cloud Gateway It is the official open source library of spring cloud, which can build API gateway on spring MVC

How to access

By modifying the official example nacos-gateway-example To demonstrate the functions of API gateway

Modify POM XML file, introducing Nacos Discovery Starter and Spring Cloud Gateway Starter dependencies

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

Configure the Nacos Server address and Spring Cloud Gateway route in the configuration file

Spring Cloud Gateway can be configured in two ways:

  • application.yml profile mode
  • Return value of RouteLocator method through @ Bean annotation
spring:
  main:
    #Spring cloud gateway is implemented internally through netty + weblux
    #Weblux implementation and spring boot starter web dependency conflict
    web-application-type: reactive

  application:
    name: nacos-gateway-discovery

  cloud:
    #Nacos config
    nacos:
      username: nacos
      password: nacos
      discovery:
        server-addr: 127.0.0.1:8848

    #spring cloud gateway config
    gateway:
      routes:
        - id: nacos-gateway  
          uri: lb://nacos-discovery-provider
          # The / nacos of the gateway is mapped to the nacos discovery provider service
          predicates:
            - Path=/nacos/**
          filters:
            - StripPrefix=1

Use the @ EnableDiscoveryClient annotation to enable service registration and discovery

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

Service startup

  • Start the Nacos discovery provider service
  • Start the gateway service of this instance

Execute at this time http://192.168.123.100:18061/nacos/ **The request is actually forwarded to the Nacos discovery provider service, as shown in the following figure:

#Because StripPrefix=1 is used
#/ echo/aaa actually forwarded to the Nacos Discovery Provider Service 
curl 'http://192.168.123.100:18061/nacos/echo/aaa' 
 
hello Nacos Discovery aaa

Global exception handling

In spring cloud gateway, DefaultErrorWebExceptionHandler is used by default to handle exceptions. This can be obtained through the configuration class ErrorWebFluxAutoConfiguration.

The default exception handling logic in the DefaultErrorWebExceptionHandler class is as follows:

public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
 ...
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
    }
   ...
}

Confirm what resource format is returned according to the request header.

The returned data content is constructed in the DefaultErrorAttributes class.

public class DefaultErrorAttributes implements ErrorAttributes {
 ...
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        Throwable error = this.getError(request);
        MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations.from(error.getClass(), SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class);
        HttpStatus errorStatus = this.determineHttpStatus(error, responseStatusAnnotation);
        errorAttributes.put("status", errorStatus.value());
        errorAttributes.put("error", errorStatus.getReasonPhrase());
        errorAttributes.put("message", this.determineMessage(error, responseStatusAnnotation));
        errorAttributes.put("requestId", request.exchange().getRequest().getId());
        this.handleException(errorAttributes, this.determineException(error), includeStackTrace);
        return errorAttributes;
    }
 ...
}

After reading here, you can see why the above data format is returned. Next, you need to rewrite the return format.

Here, you can customize a CustomErrorWebExceptionHandler class to inherit DefaultErrorWebExceptionHandler, and then modify the logic of generating front-end response data. Then define a configuration class, which can be written with reference to ErrorWebFluxAutoConfiguration. Simply replace the exception class with CustomErrorWebExceptionHandler class.

Please study this method by yourself. It is basically copying code. Rewriting is not complicated. This method will not be demonstrated. Here is another writing method:

Define a global exception class GlobalErrorWebExceptionHandler, let it directly implement the top-level interface ErrorWebExceptionHandler, override the handler() method, and return a custom response class in the handler() method. However, it should be noted that the priority of the rewritten implementation class must be less than that of the built-in ResponseStatusExceptionHandler, which processes the response code of the corresponding error class.

The code is as follows:

/**
 * Gateway global exception handling
 */
@Slf4j
@Order(-1)
@Configuration
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        }

        // Set return JSON
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        if (ex instanceof ResponseStatusException) {
            response.setStatusCode(((ResponseStatusException) ex).getStatus());
        }

        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            try {
                //Return response results
                return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500,ex.getMessage())));
            }
            catch (JsonProcessingException e) {
                log.error("Error writing response", ex);
                return bufferFactory.wrap(new byte[0]);
            }
        }));
    }
}

The Privacy Interface prohibits external access

How to prevent the internal privacy interface from being called by the gateway in the spring cloud system? The solutions mainly include:

Blacklist mechanism

Store these interfaces in the "blacklist", read the blacklist configuration when the gateway starts, and then verify whether they are in the blacklist

Interface path

That is, when specifying the access path to the interface, the format is: / access control / interface. Access control can have the following rules (refer to JAVA package specification), which can be extended according to business needs.

pb - public All requests are accessible

pt - protected Need to token Access is only possible after authentication

pv - private The micro service can only be accessed through the micro gateway

df - default Gateway request token Authentication, and request parameters and return results for encryption and decryption

...

With this set of interface specifications, you can flexibly control the access rights of the interface, and then verify the interface path at the gateway. If the corresponding access control rules are hit, the corresponding logic processing will be carried out.

@Component
@Order(0)
@Slf4j
public class GatewayRequestFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //Get request path
        String rawPath = exchange.getRequest().getURI().getRawPath();

        if(isPv(rawPath)){
            throw new HttpServerErrorException(HttpStatus.FORBIDDEN,"can't access private API");
        }
        
        return chain.filter(newExchange);
    }

    /**
     * Determine whether the internal private method
     * @param requestURI Request path
     * @return boolean
     */
    private boolean isPv(String requestURI) {
        return isAccess(requestURI,"/pv");
    }

    /**
     * Gateway access control verification
     */
    private boolean isAccess(String requestURI, String access) {
        //The back-end standard request path is / access control / request path
        int index = requestURI.indexOf(access);
        return index >= 0 && StringUtils.countOccurrencesOf(requestURI.substring(0,index),"/") < 1;
    }
}

Gray Publishing

  • By implementing ServiceInstanceListSupplier to customize the service filtering logic, you can directly inherit DelegatingServiceInstanceListSupplier to implement it
/**
 * Reference: org springframework. cloud. loadbalancer. core. ZonePreferenceServiceInstanceListSupplier
 */
@Log4j2
public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {


    public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
        super(delegate);
    }


    @Override
    public Flux<List<ServiceInstance>> get() {
        return delegate.get();
    }

    @Override
    public Flux<List<ServiceInstance>> get(Request request) {
        return delegate.get(request).map(instances -> filteredByVersion(instances,getVersion(request.getContext())));
    }


    /**
     * filter instance by requestVersion
     */
    private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String requestVersion) {
        log.info("request version is {}",requestVersion);
        if(StringUtils.isEmpty(requestVersion)){
            return instances;
        }

        List<ServiceInstance> filteredInstances = instances.stream()
                .filter(instance -> requestVersion.equalsIgnoreCase(instance.getMetadata().getOrDefault("version","")))
                .collect(Collectors.toList());

        if (filteredInstances.size() > 0) {
            return filteredInstances;
        }

        return instances;
    }

    private String getVersion(Object requestContext) {
        if (requestContext == null) {
            return null;
        }
        String version = null;
        if (requestContext instanceof RequestDataContext) {
            version = getVersionFromHeader((RequestDataContext) requestContext);
        }
        return version;
    }

    /**
     * get version from header
     */
    private String getVersionFromHeader(RequestDataContext context) {
        if (context.getClientRequest() != null) {
            HttpHeaders headers = context.getClientRequest().getHeaders();
            if (headers != null) {
                //could extract to the properties
                return headers.getFirst("version");
            }
        }
        
        return null;
    }
}

The implementation principle is the same as the custom load balancing strategy, matching the qualified service instances according to the version.

  • Write the configuration class VersionServiceInstanceListSupplierConfiguration to replace the default service instance filtering logic
public class VersionServiceInstanceListSupplierConfiguration {
    @Bean
    ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) {
        ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
                .withDiscoveryClient()
                .withCaching()
                .build(context);
        return new VersionServiceInstanceListSupplier(delegate);
    }
}
  • Use the annotation @ LoadBalancerClient in the gateway startup class to specify which services use the custom load balancing algorithm
    Through @ loadbalancerclient (value = "Nacos Discovery Provider", configuration = versionserviceinstancelistsupplierconfiguration. Class), Enable custom load balancing algorithms for Nacos Discovery Provider or through @ LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class) for all services

reference resources

Introduction to routing and forwarding rules of Spring Cloud GateWay

Spring Cloud Gateway: a new generation API gateway service

nacos-gateway-example

The Privacy Interface prohibits external access

Realize gray publishing of gateway

Keywords: Spring Cloud Microservices gateway

Added by jerdo on Sat, 12 Feb 2022 03:01:04 +0200