[JAVA] Une petite main qui lit un corps JSON à partir de ServerHttpRequest avec un WebFilter auto-créé, etc. avec Spring WebFlux

Spring Boot 2.3.4 C'est l'histoire de l'environnement au moment de la sortie.


Spring WebFlux propose plusieurs façons de gérer les requêtes HTTP.

Pour générer des données de réponse HTTP à ce moment Dans la plupart des cas, vous utiliserez probablement RouterFunction ou @ Controller.

ServerRequest est passé à RouterFunction, donc si vous appelezServerRequest # bodyToMono (Class <? Extends T>) Vous pouvez analyser les données JSON du corps de la requête.

Dans Spring Boot, dans le cas de @ Controller, si l'annotation @ RequestBody est ajoutée Il analyse le corps de la requête côté framework.

Dans la plupart des cas, cette méthode peut convertir le JSON du corps de la requête en un objet à utiliser dans l'application.

Cependant, l'authentification personnalisée Spring Security est traitée au stade WebFilter, donc «RouterFunction» etc. sont appelés.

À ce stade, ServerWebExchange est passé à WebFilter, mais la méthodeServerWebExchange # getRequest ()est Renvoie ServerHttpRequest.

Puisque ServerHttpRequest n'a pas de relation d'héritage avec ServerRequest, Il n'a pas de fonction pratique comme ServerRequest # bodyToMono (Class <? Extends T>).

Vous pouvez obtenir des données de formulaire avec ServerWebExchange # getFormData (), mais vous devez analyser vous-même le JSON du corps de la requête.

Pour rendre ce processus d'analyse similaire à Spring Boot, récupérez le ServerCodecConfigurer avec ʻAutowired ObtenezListSE <HttpMessageReader <? >>avecServerCodecConfigurer # getReaders (), Le HttpMessageReader pour analyser le corps de la requête HTTP doit être obtenu avec Stream # filter ()`, et ainsi de suite.

Si cela est écrit dans ʻorg.springframework.security.web.server.authentication.ServerAuthenticationConverter` de Spring Security, ce sera à peu près comme suit.

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-Vérification de type.
        if (contentType == null || acceptType.isCompatibleWith(contentType)) {
            return Mono.error(new AuthenticationException("Invalid Content-Type"));
        }

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

        //JsonParameter est le nom d'utilisateur,Une classe qui stocke simplement le mot de passe dans le champ.
        //JsonAuthentication est une sous-classe d'authentification qui ne stocke que JsonParameters.
        //La définition est omise pour les deux.
        return this.httpMessageReaders.stream()
            // Content-Type: application/json demande des données à JsonParameter.Peut être converti en classe
            //Recherchez une implémentation HttpMessageReader.
            .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 +
                ")";
        }

    }
}

Pour être honnête, je trouve cette implémentation assez lourde.

E / S asynchrones pour gérer les corps de requête qui ne savent pas quand ils seront envoyés, Parce qu'il n'est pas corrigé avant l'exécution, ce qui sera utilisé pour l'objet qui traite le corps du message dans Spring Boot Je peux comprendre cela.

Cependant, même si vous soustrayez qu'il s'agit d'un traitement de message HTTP unique qui est différent de l'API Servert Ecrire cette procédure à chaque fois semblait très ennuyeux.

Puisque «Mono» est utilisé, il est un peu gênant de préparer une fonction utilitaire pratique.

J'ai pensé, mais un jour j'ai remarqué que la fonction ServerRequest # create (ServerWebExchange, List <HttpMessageReader <? >>) existe. La ServerRequest générée par cette fonction provient de la List <HttpMessageReader <? >> Donner au traitement du corps du message. Il récupérera et utilisera le HttpMessageReader approprié.

Ce en quoi vous voulez convertir le corps du message est simplement de générer BodyExtractor à partir de la fonction fournie dans BodyExtractors.

La quantité de code peut être considérablement réduite même avec le code précédent. La vérification de Content-Type est laissée au cas où.

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 <? >>) est une fonctionnalité utile de WebFilter, Pas souvent introduit.

Recommended Posts

Une petite main qui lit un corps JSON à partir de ServerHttpRequest avec un WebFilter auto-créé, etc. avec Spring WebFlux
Comment lire le corps de la requête plusieurs fois avec Spring Boot + Spring Security
Vous utilisez le contexte pour utiliser MDC avec Spring WebFlux
Comment obtenir JDK etc. depuis Oracle avec CLI