Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Custom Filters in Spring Cloud Gateway 2.x: GatewayFilter, GlobalFilter, and Filter Factories

Tech 1
  • Spring Cloud release train: Greenwich.SR3 (Spring Cloud Gateway 2.1.x)
  • Spring Boot 2.1.x
  • Sample setup assumes Eureka Server, Eureka Ribbon client, Eureka Feign client, and Spring Cloud Gateway

GatewayFilter: per-route filtering

Implement a route-scoped filter by implementing GatewayFilter and Ordered. Ordered determines precedence (smaller values run earlier).

package com.example.gateway.filters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class TimingGatewayFilter implements GatewayFilter, Ordered {
    private static final Logger log = LoggerFactory.getLogger(TimingGatewayFilter.class);
    private static final String START_NANOS = "timingStartNanos";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(START_NANOS, System.nanoTime());
        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                Long started = exchange.getAttribute(START_NANOS);
                if (started != null) {
                    long elapsedMs = (System.nanoTime() - started) / 1_000_000;
                    log.info("request={} took={}ms", exchange.getRequest().getURI(), elapsedMs);
                }
            }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

Attach the filter to a route definition using RouteLocatorBuilder:

package com.example.gateway.config;

import com.example.gateway.filters.TimingGatewayFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RoutesConfiguration {

    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("ribbon-route", r -> r
                .order(0)
                .path("/ribbon/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .filter(new TimingGatewayFilter())
                    .addResponseHeader("X-Demo-Response", "gw-2x"))
                .uri("lb://EUREKA-RIBBON"))
            .build();
    }
}

Pre vs. Post logic in a filter

  • Code before invoking chain.filter(exchange) is the pre phase.
  • Logic chained via then(...) runs after the downstream handler completes and is the post phase.

GlobalFilter: cross-route filtering

A GlobalFilter applies to every route (subject to its order). Example: reject requests missing a token query parameter.

package com.example.gateway.filters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class TokenGuardFilter implements GlobalFilter, Ordered {
    private static final Logger log = LoggerFactory.getLogger(TokenGuardFilter.class);
    private static final String TOKEN_PARAM = "token";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst(TOKEN_PARAM);
        if (token == null || token.trim().isEmpty()) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 1; // runs early, before LOWEST_PRECEDENCE filters
    }
}

Optional bean registration alternative if component scanning isn’t used:

package com.example.gateway.config;

import com.example.gateway.filters.TokenGuardFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FiltersRegistration {
    @Bean
    public TokenGuardFilter tokenGuardFilter() {
        return new TokenGuardFilter();
    }
}

Run and verify

GatewayFilterFactory: reusable, configurable filters

Create a custom factory for reusable filters with configuration bound from YAML shortcuts.

package com.example.gateway.filters.factory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;

public class RequestMetricsGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestMetricsGatewayFilterFactory.Config> {
    private static final Logger log = LoggerFactory.getLogger(RequestMetricsGatewayFilterFactory.class);
    private static final String ATTR_REQ_START = "reqMetricsStart";

    public RequestMetricsGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("logParams");
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            exchange.getAttributes().put(ATTR_REQ_START, System.currentTimeMillis());
            return chain.filter(exchange)
                .then(Mono.fromRunnable(() -> {
                    Long t0 = exchange.getAttribute(ATTR_REQ_START);
                    if (t0 == null) {
                        return;
                    }
                    long took = System.currentTimeMillis() - t0;
                    StringBuilder sb = new StringBuilder()
                        .append("uri=").append(exchange.getRequest().getURI())
                        .append(", time=").append(took).append("ms");
                    if (config.isLogParams()) {
                        sb.append(", params=").append(exchange.getRequest().getQueryParams());
                    }
                    log.info(sb.toString());
                }));
        };
    }

    public static class Config {
        private boolean logParams;
        public boolean isLogParams() { return logParams; }
        public void setLogParams(boolean logParams) { this.logParams = logParams; }
    }
}

Register the factory as a Spring bean:

package com.example.gateway.config;

import com.example.gateway.filters.factory.RequestMetricsGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterFactoryConfig {
    @Bean
    public RequestMetricsGatewayFilterFactory requestMetricsGatewayFilterFactory() {
        return new RequestMetricsGatewayFilterFactory();
    }
}

Enable and use the factory in configuration

application.yml example with discovery locator and default/route filters:

server:
  port: 8100
spring:
  application:
    name: spring-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      default-filters:
        - RequestMetrics=true   # enable custom factory; logs params when true
      routes:
        - id: ribbon-route
          uri: lb://EUREKA-RIBBON
          order: 0
          predicates:
            - Path=/ribbon/**
          filters:
            - StripPrefix=1
            - AddResponseHeader=X-Demo-Response, gw-2x
        - id: feign-route
          uri: lb://EUREKA-FEIGN
          order: 0
          predicates:
            - Path=/feign/**
          filters:
            - StripPrefix=1
            - AddResponseHeader=X-Demo-Response, gw-2x

eureka:
  instance:
    hostname: eureka1.server.com
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 10
  client:
    service-url:
      defaultZone: http://eureka1.server.com:8701/eureka/,http://eureka2.server.com:8702/eureka/,http://eureka3.server.com:8703/eureka/

Visit http://localhost:8100/ribbon/sayHello?token=xyz and observe a log entry similar to:

  • RequestMetricsGatewayFilterFactory: uri=http://localhost:8100/ribbon/sayHello?token=xyz, time=NNms, params:{token=[xyz]}

Notes

  • GlobalFilter and GatewayFilter share the same method signature; GlobalFilter instances can affect all routes.
  • When extending AbstractGatewayFilterFactory, pass the custom Config class too the super constructor so the frmaework can bind properties from YAML.
  • AbstractGatewayFilterFactory binds properties based on shortcutFieldOrder; a single boolean maps clean to the YAML form: RequestMetrics=true.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.