Is there a way to apply a filter or code BEFORE routing (Spring Cloud Gateway)

3.6k views Asked by At

I'm writing an API Gateway that must route requests based on a MAC address. Example of endpoints:

/api/v2/device/AABBCCDDEEFF
/api/v2/device/AABBCCDDEEFF/metadata
/api/v2/device/search?deviceId=AABBCCDDEEFF

I've written a Custom Predicate Factory that extracts the MAC address, performs the necessary logic to determine what URL the MAC address should be routed to, then stores that information on the ServerWebExchange attributes.

public class CustomRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomRoutePredicateFactory.Config> {
    // Fields/Constructors Omitted

    private static final String IP_ATTRIBUTE = "assignedIp";
    private static final String MAC_ATTRIBUTE = "mac";

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return (ServerWebExchange exchange) -> {
            String mac = exchange.getAttributes().get(MAC_ATTRIBUTE);
            if(mac == null){
                mac = extractMacAddress(exchange);
            }

            if(!exchange.getAttributes().contains(IP_ATTRIBUTE)){
                exchange.getAttributes().put(IP_ATTRIBUTE, findAssignedIp(mac);
            }

            return config.getRouteIp().equals(exchange.getAttribute(IP_ATTRIBUTE));
        });
    }
    // Config Class & utility methods omitted
}

NOTE: This implementation is greatly simplified for brevity

With this implementation I'm able to guarantee that the MAC is extracted only once and the logic determining what URL the request belongs to is performed only once. The first call to the predicate factory will extract and set that information on ServerWebExchange Attributes and any further calls to the predicate factory will detect those attributes and use them to determine if they match.

This works, but it isn't particularly neat. It would be much easier and simpler if I could somehow set the Exchange Attributes on every single request entering the gateway BEFORE the application attempts to match routes. Then the filter could be a simple predicate that checks for equality on the Exchange Attributes.

I've read through the documentation several times, but nothing seems to be possible. Filters are always scoped to a particular route and run only after a route matches. It might be possible to make the first route be another Predicate that executes the necessary code, sets the expected attributes and always returns false, but can I guarantee that this predicate is always run first? It seems like there should be support for this kind of use case, but I cannot for the life of me find a way that doesn't seem like a hack. Any ideas?

3

There are 3 answers

1
kayani On

I think your approach makes sense since you want it to run before filters.

Have you considered using a GlobalFilter with an order set on it? You can ensure it's always the first filter to run. You can also modify the URL in the ServerWebExchange by mutating the request and setting the GATEWAY_REQUEST_URL_ATTR attribute on the exchange.

Take a look at the PrefixPathGatewayFilterFactory for an example of how to change the URI being routed to.

You can set an order on the Global filter by implementing the org.springframework.core.Ordered interface.

That being said, it still feels a little like a hack but it's an alternative approach.

0
pigman On

i think it may help you that overriding the class RoutePredicateHandlerMapping. see: org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandler

1
Sergey Klimtsev On

Use a WebFilter instead of a GatewayFilter or a GlobalFilter. They are applied only after the predicates chain. Whereas WebFilter works as an interceptor.

@Component
public class CustomRoutePredicateFactory implements WebFilter, Ordered {

    private static final String IP_ATTRIBUTE = "assignedIp";
    private static final String MAC_ATTRIBUTE = "mac";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String mac = (String) exchange.getAttributes().computeIfAbsent(MAC_ATTRIBUTE, key -> extractMacAddress(exchange));
        exchange.getAttributes().computeIfAbsent(IP_ATTRIBUTE, key -> findAssignedIp(mac));
        return chain.filter(exchange);
    }

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