spring-webfluxspring-cloud-gatewayspring-reactivespring-data-r2dbcr2dbc

Check database for access in Spring Gateway Pre filter


I am using Spring Gateway, where I need to check further user access by Request path using DB call. My repository is like this.

public Mono<ActionMapping> getByUri(String url)
....

This is my current filter where I am using custom UsernamePasswordAuthenticationToken implementation.

@Override
public GatewayFilter apply(Config config) {

    return (exchange, chain) -> exchange
        .getPrincipal()
        .filter(principal -> principal instanceof UserAuthenticationToken) // Custom implementation of UsernamePasswordAuthenticationToken
        .cast(UserAuthenticationToken.class)
        .map(userAuthenticationToken -> extractAuthoritiesAndSetThatToRequest(exchange, userAuthenticationToken))
        .defaultIfEmpty(exchange)
        .flatMap(chain::filter);
}

private ServerWebExchange extractAuthoritiesAndSetThatToRequest(ServerWebExchange exchange, UserAuthenticationToken authentication) {

    var uriActionMapping = uriActionMappingRepository.findOneByUri(exchange.getRequest().getPath().toString()).block();

    if ((uriActionMapping == null) || (authentication.getPermission().containsKey(uriActionMapping.getName()))) {

        ServerHttpRequest request = exchange.getRequest()
                                            .mutate()
                                            .header("X-Auth", authentication.getName())
                                            .build();

        return exchange.mutate().request(request).build();
    }

    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(HttpStatus.UNAUTHORIZED);
    response.setComplete();

    return exchange.mutate().response(response).build();
}

However, there are several problems here, first that it is blocking call. Also I am not sure I need to mutate exchange to return response like that. Is there anyway achieve this using filter in Spring Cloud Gateway.


Solution

  • Yes, it is a blocking call.

    Firstly, Spring WebFlux is based on Reactor. In Reactor, most handling method will not recieve a null from Mono emit, e.g. map, flatMap. Sure, there are counterexamples, such as doOnSuccess, see also the javadoc of Mono.

    So, we can just use handling methods to filter results instead of block. Those handling methods will return a empty Mono when recieve a null value.

    Secondary, when it authorize failed, we should return a empty Mono instead of calling chain.filter. The chain.filter means "It's OK! Just do something after the Filter!". See also RequestRateLimiterGatewayFilterFactory, it also mutate the response.

    So, we should set response to completed, and return a empty Mono if authorize failed.

    Try this:

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> exchange
            .getPrincipal()
            .filter(principal -> principal instanceof UserAuthenticationToken) // Custom implementation of UsernamePasswordAuthenticationToken
            .cast(UserAuthenticationToken.class)
            .flatMap(userAuthenticationToken -> extractAuthoritiesAndSetThatToRequest(exchange, userAuthenticationToken))
            .switchIfEmpty(Mono.defer(() -> exchange.getResponse().setComplete().then(Mono.empty())))
            .flatMap(chain::filter);
    }
    
    // Maybe return empty Mono, e.g. findOneByUri not found, or Permissions does not containing
    private Mono<ServerWebExchange> extractAuthoritiesAndSetThatToRequest(ServerWebExchange exchange, UserAuthenticationToken authentication) {
        return uriActionMappingRepository.findOneByUri(exchange.getRequest().getPath().toString())
            .filter(it -> authentication.getPermission().containsKey(it.getName()))
            .map(it -> exchange.mutate()
                .request(builder -> builder.header("X-Auth", authentication.getName()))
                .build());
    }
    

    About mutate request, see also RewritePathGatewayFilterFactory.