Short hand to read JSON body from ServerHttpRequest with self-made WebFilter etc. with Spring WebFlux

Spring Boot 2.3.4. This is the story of the environment at the time of RELEASE.


Spring WebFlux provides several ways to handle HTTP requests.

To generate HTTP response data at this time In most cases, you will probably use RouterFunction or @Controller.

ServerRequest is passed to RouterFunction, so if you call ServerRequest # bodyToMono (Class <? Extends T>) You can parse the JSON data of the request body.

In Spring Boot, in the case of @Controller, if @RequestBody annotation is added It parses the request body on the framework side.

In most cases, you can use this method to convert the JSON of the request body into an object for use in your application.

However, Spring Security custom authentication is processed at the WebFilter stage, so RouterFunction etc. are called.

At this time, ServerWebExchange is passed to WebFilter, but the ServerWebExchange # getRequest () method is Returns ServerHttpRequest.

Since ServerHttpRequest does not have an inheritance relationship with ServerRequest, It doesn't have a convenient function like ServerRequest # bodyToMono (Class <? Extends T>).

You can get form data with ServerWebExchange # getFormData (), but you have to parse the JSON of the request body yourself.

To make this parsing process similar to Spring Boot, get ServerCodecConfigurer with ʻAutowiredand get it. GetListSE <HttpMessageReader <? >>withServerCodecConfigurer # getReaders (), The HttpMessageReader for parsing the HTTP request body needs to be obtained by Stream # filter () `, and so on.

If you write this in Spring Security's ʻorg.springframework.security.web.server.authentication.ServerAuthenticationConverter`, it will be roughly as follows.

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.naming.AuthenticationException;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class JsonLoginServerAuthenticationConverter implements ServerAuthenticationConverter {

    private final List<HttpMessageReader<?>> httpMessageReaders;

    public JsonLoginServerAuthenticationConverter(ServerCodecConfigurer serverCodecConfigurer) {
        this(serverCodecConfigurer.getReaders());
    }

    public JsonLoginServerAuthenticationConverter(List<HttpMessageReader<?>> httpMessageReaders) {
        this.httpMessageReaders = Objects.requireNonNull(httpMessageReaders);
    }

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {

        ServerHttpRequest request = exchange.getRequest();
        MediaType contentType = request.getHeaders().getContentType();
        MediaType acceptType = MediaType.APPLICATION_JSON;

        // Content-Type check.
        if (contentType == null || acceptType.isCompatibleWith(contentType)) {
            return Mono.error(new AuthenticationException("Invalid Content-Type"));
        }

        ResolvableType resolvableType = ResolvableType.forClass(JsonParameter.class);

        //JsonParameter is username,A class that just stores password in the field.
        //JsonAuthentication is a subclass of Authentication that only stores JsonParameters.
        //Definitions are omitted for both.
        return this.httpMessageReaders.stream()
            // Content-Type: application/Json request data to JsonParameter.Can be converted to class
            //Find the HttpMessageReader implementation.
            .filter(httpMessageReader ->
                httpMessageReader.canRead(resolvableType, acceptType))
            .findFirst()
            .orElseThrow(() -> new IllegalStateException("Could not read JSON data."))
            .readMono(resolvableType, request, Collections.emptyMap())
            .cast(JsonParameter.class)
            .map(JsonAuthentication::new);
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    static class JsonParameter {
        private final String username;

        private final String password;

        @JsonCreator
        public JsonParameter(
            @JsonProperty("username") String username,
            @JsonProperty("password") String password) {
            this.username = username;
            this.password = password;
        }

        public String getUsername() {
            return this.username;
        }

        public String getPassword() {
            return this.password;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            JsonParameter that = (JsonParameter) o;
            return Objects.equals(username, that.username) &&
                Objects.equals(password, that.password);
        }

        @Override
        public int hashCode() {
            return Objects.hash(username, password);
        }

        @Override
        public String toString() {
            return "JsonParameter(" +
                "username=" + this.username +
                ", password=" + this.password +
                ")";
        }

    }
}

To be honest, I find this implementation quite cumbersome.

Asynchronous I / O to handle request bodies that do not know when they will be sent, Because it is not fixed until runtime what will be used for the object that processes the message body in Spring Boot I can understand this.

However, even if you subtract that it is a unique HTTP message processing that is different from the Servert API Writing this procedure every time seemed very annoying.

Since Mono is used, it is a little troublesome to prepare a convenient utility function.

I thought, but one day I noticed that the ServerRequest # create (ServerWebExchange, List <HttpMessageReader <? >>) function exists. The ServerRequest generated by this function is from theList <HttpMessageReader <? >>Give to the processing of the message body. It will retrieve and use the appropriate HttpMessageReader.

What you want to convert the message body to is just to generate BodyExtractor from the function provided in BodyExtractors.

The amount of code can be greatly reduced even with the previous code. The check for Content-Type is left just in case.

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class JsonLoginServerAuthenticationConverter implements ServerAuthenticationConverter {

    private final List<HttpMessageReader<?>> httpMessageReaders;

    public JsonLoginServerAuthenticationConverter(ServerCodecConfigurer serverCodecConfigurer) {
        this(serverCodecConfigurer.getReaders());
    }

    public JsonLoginServerAuthenticationConverter(List<HttpMessageReader<?>> httpMessageReaders) {
        this.httpMessageReaders = Objects.requireNonNull(httpMessageReaders);
    }

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        MediaType contentType = request.getHeaders().getContentType();

        if (contentType == null || MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
            return Mono.error(new AuthenticationException("Invalid Content-Type"));
        }

        return ServerRequest.create(exchange, this.httpMessageReaders)
            .body(BodyExtractors.toMono(JsonParameter.class))
            .map(JsonAuthentication::new);
    }
}

ServerRequest # create (ServerWebExchange, List <HttpMessageReader <? >>) is a useful feature in WebFilter, Not often introduced.

Recommended Posts

Short hand to read JSON body from ServerHttpRequest with self-made WebFilter etc. with Spring WebFlux
How to read Body of Request multiple times with Spring Boot + Spring Security
You use context to use MDC with Spring WebFlux
How to get jdk etc from oracle with cli