spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.54k stars 3.33k forks source link

How to Validate a Specific Value in the Request Body of a POST Request in Spring Cloud Gateway MVC? #3512

Closed marinusgeuze closed 1 month ago

marinusgeuze commented 2 months ago

Hi,

In our Spring Cloud Gateway MVC project, we need to validate a specific value in the request body of a POST request before routing it to a downstream microservice.

The only solution we've found that works is the code snippet below. However, it feels somewhat cumbersome, and we're wondering if there's a cleaner, more idiomatic way to achieve this. We couldn't find a better approach in the Spring Cloud Gateway MVC documentation.

Could anyone suggest a more elegant solution?

Thanks in advance for your help!

Router configuration

    return route()
        .POST(
            “path”
            https())
        .before(validate())
        .build();
  }

Before filter

public static Function<ServerRequest, ServerRequest> validate() {

    return request -> {

      try {
        var clonedInputStream = cloneInputStream(request);
        var clonedRequest = adaptCachedBody().apply(request);

        Map<String, String> jsonRequest = new ObjectMapper().readValue(clonedInputStream, Map.class);

        // Reset stream so that it can be read again when request is routed to downstream MicroService
        clonedInputStream.reset();

        var field = jsonRequest.get("field");

        //Validate logic

        return clonedRequest;

      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    };
  }

private static InputStream cloneInputStream(ServerRequest request) throws IOException {

  var inputStream = request.servletRequest().getInputStream();
  var byteArray = StreamUtils.copyToByteArray(inputStream);
  var clonedInputStream = new ByteArrayInputStream(byteArray);

  // This attribute is read by the adaptCachedBody function
  request.attributes().put(MvcUtils.CACHED_REQUEST_BODY_ATTR, clonedInputStream);

  return clonedInputStream;
}
spencergibb commented 1 month ago

You can use the ModifyRequestBody filter and just return the body unmodified https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories/modifyrequestbody-factory.html#page-title

Otherwise you could also use the CacheRequestBody filter https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories/cacherequestbody-factory.html

marinusgeuze commented 1 month ago

Dear Spencer,

Thanks for your answer.

We are using Spring Cloud Gateway MVC version. These filters seem to be for the reactive version. Are there servlet versions of these filters?

Thanks in advance.

Marinus

Ps. I really hope you have time to answer question https://github.com/spring-cloud/spring-cloud-gateway/issues/3449 too. :-)

spencergibb commented 1 month ago

Indeed, use MvcUtils.cacheAndReadBody https://github.com/spring-cloud/spring-cloud-gateway/blob/2295b9aaab18f72b78453d108a466a0d1ff9f494/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java#L102

In the mvc gateway, if you read the body, you need to make it available again with BodyFilterFunctions.adaptCachedBody. https://github.com/spencergibb/spring-cloud-gateway-mvc-sample/blob/42c6840ca5b162478dd934b9e85bfad694c01873/src/main/java/com/example/gatewaymvcsample/Route06ReadBodyPredicate.java#L24

marinusgeuze commented 1 month ago

Dear Spencer,

Thanks a lot for your answer. Now we can remove our own cloneInputStream code.