Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to extract request body in ReactiveAuthorizationManager of Spring WebFlux Security?

I need to extract the request body from the ServerHttpRequest in my custom implementation of ReactiveAuthorizationManager<AuthorizationContext>. The body payload contains an id of an entity. I need to check the request's authorization to see if the requester can access the entity.

To implement it I created a custom ServerHttpRequestDecorator:

public class BodyInterceptingRequest extends ServerHttpRequestDecorator {

    private StringBuilder body = null;

    public BodyInterceptingRequest(ServerHttpRequest delegate) {
        super(delegate);
    }

    public Flux<DataBuffer> getBody() {
        if (body == null) {
            return super.getBody();
        }

        DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
        byte[] bytes = getRequestBody().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
        return Flux.just(buffer.write(bytes));
    }

    public Mono<String> readBody() {
        // @formatter:off
        return DataBufferUtils.join(super.getBody())
            .map((DataBuffer dataBuffer) -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            })
            .defaultIfEmpty(new byte[0])
            .flatMap(bytes -> {
                body = new StringBuilder();
                body.append(new String(bytes, StandardCharsets.UTF_8));
                return Mono.just(body.toString());
            });
        // @formatter:on
    }

    public String getRequestBody() {
        return this.body.toString();
    }
}

And used this decorator in a custom ServerWebExchangeDecorator:

public class RequestBodyInterceptingExchange extends ServerWebExchangeDecorator {

    private final BodyInterceptingRequest request;

    public RequestBodyInterceptingExchange(ServerWebExchange exchange) {
        super(exchange);
        this.request = new BodyInterceptingRequest(exchange.getRequest());
    }

    @Override
    public BodyInterceptingRequest getRequest() {
        return request;
    }
}

I also have created a custom WebFilter:

public class RequestBodyInterceptingFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(new RequestBodyInterceptingExchange(exchange));
    }
}

And registered it in ServerHttpSecurity as:

http
    // other stuff
    .addFilterBefore(new RequestBodyInterceptingFilter(), SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE)
    .build();

Now, the above-mentioned custom implementation of ReactiveAuthorizationManager<AuthorizationContext> looks like this:

public class MyAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
        // @formatter:off
        return Mono.zip(
                authentication,
                getId(context)
            )
            .flatMap((Tuple2<Authentication, String> tuple) -> {
                Authentication auth = tuple.getT1();
                String id = tuple.getT2();
                Jwt principal = (Jwt) auth.getPrincipal();
                String userId = principal.getSubject();

                return // execute some logic and return a Mono boolean
            })
            .filter(BooleanUtils::isTrue)
            .map(AuthorizationDecision::new);
        // @formatter:on
    }

    protected Mono<String> getId(AuthorizationContext context) {
        SecurityContextServerWebExchange securityContextExchange = (SecurityContextServerWebExchange) context.getExchange();
        RequestBodyInterceptingExchange exchange = (RequestBodyInterceptingExchange) securityContextExchange.getDelegate();

        // @formatter:off
        return exchange.getRequest()
            .readBody()
            .flatMap((String requestBody) -> Mono.just(convert(requestBody).getId())); // convert is a method for deserialize JSON to an Object
        // @formatter:on
    }
}

I can achieve what I need. But I have a few questions:

  • Is it the right way to do what I intended to do?
  • Is there any chance that the class BodyInterceptingRequest is buggy and in case of the concurrent hit, it provides misinformation? I mean, if there are two callers, caller1 and caller2 and they invoke the API with request1 and request2 respectively, then the request2 gets processed for caller1 and the request1 gets processed for caller2.
  • Is there any chance of memory leak through the implementation of BodyInterceptingRequest?

I appreciate your expert opinion about my approach.

like image 249
Tapas Bose Avatar asked Oct 24 '25 14:10

Tapas Bose


1 Answers

Is it the right way to do what I intended to do?

Actually, this is not the best idea in a reactive application. Your case is a bit tricky because ReactiveAuthorizationManager is often used to perform authorization based on the data like user roles, claims, etc., not the request body. The tricky moment here is that reading the request body for authorization means you are consuming the Flux<DataBuffer>. Since this Flux can only be consumed once, you need to cache it and forward the cache to the controller. And this is exactly what you've already done in your example.

The problem here is that such caching can negate some benefits of WebFlux. For example, in this context, it makes caching involves storing the entire request body in memory so that it can be read multiple times. However, one of the benefits of using a stream-based, non-blocking I/O model like WebFlux is the ability to handle data by processing it in chunks as they arrive (that's why you have Flux<DataBuffer> and not Mono<DataBuffer>). Caching it may affect application performance, especially with large request bodies. Also, caching isn't an option if the request body is a data stream. A stream might not be available all at once, so you can't cache it.

That's why it is not recommended to do what you want to do. Better to move all needed authorization context to headers and check headers.

Is there any chance that the class BodyInterceptingRequest is buggy and in case of the concurrent hit, it provides misinformation? I mean, if there are two callers, caller1 and caller2 and they invoke the API with request1 and request2 respectively, then the request2 gets processed for caller1 and the request1 gets processed for caller2.

However, if you understand all cons but want to proceed with such a solution, the general implementation is correct. It is unlikely that you will encounter the situation you described because you create a new instance of BodyInterceptingRequest on each request, and you have your own "exchange" for each request. Also, WebFilters executed sequentially, so there is no race condition should be in BodyInterceptingRequest.

Is there any chance of memory leak through the implementation of BodyInterceptingRequest?

Overall, I don't see possible memory leaks here. Let's take a closer look at the important places:

  1. readBody method

According to the DataBufferUtils.join documentation:

the given data buffers do not have to be released. They will be released as part of the returned composite.

This is what you've done in DataBufferUtils.release(dataBuffer).

  1. getBody method

Since you are using DefaultDataBufferFactory, there is no need to release allocated data buffers. They will be garbage collected. But actually, I don't see a reason to allocate new memory here. I think you can replace bufferFactory.allocateBuffer with bufferFactory.wrap(bytes).

like image 75
Yevhenii Semenov Avatar answered Oct 27 '25 04:10

Yevhenii Semenov



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!