[JAVA] Eine kurze Hand, die einen JSON-Body aus ServerHttpRequest mit einem selbst erstellten WebFilter usw. mit Spring WebFlux liest

Spring Boot 2.3.4. Dies ist die Geschichte der Umgebung zum Zeitpunkt der Veröffentlichung.


Spring WebFlux bietet verschiedene Möglichkeiten, um HTTP-Anforderungen zu verarbeiten.

Zum Generieren von HTTP-Antwortdaten zu diesem Zeitpunkt In den meisten Fällen werden Sie wahrscheinlich "Router Function" oder "@ Controller" verwenden.

"ServerRequest" wird an "RouterFunction" übergeben. Wenn Sie also "ServerRequest # bodyToMono (Class <? Extends T>)" aufrufen Sie können die JSON-Daten des Anforderungshauptteils analysieren.

Im Spring Boot im Fall von "@ Controller", wenn die Annotation "@ RequestBody" hinzugefügt wird Es analysiert den Anforderungshauptteil auf der Framework-Seite.

In den meisten Fällen kann diese Methode den JSON des Anforderungshauptteils in ein Objekt zur Verwendung in der Anwendung konvertieren.

Die benutzerdefinierte Authentifizierung von Spring Security wird jedoch in der WebFilter-Phase verarbeitet RouterFunction etc. werden aufgerufen.

Zu diesem Zeitpunkt wird "ServerWebExchange" an "WebFilter" übergeben, die Methode "ServerWebExchange # getRequest ()" jedoch Gibt ServerHttpRequest zurück.

Da "ServerHttpRequest" keine Vererbungsbeziehung zu "ServerRequest" hat, Es hat keine praktische Funktion wie "ServerRequest # bodyToMono (Klasse <? Erweitert T>)".

Sie können Formulardaten mit "ServerWebExchange # getFormData ()" abrufen, müssen jedoch den JSON des Anforderungshauptteils selbst analysieren.

Um diesen Analyseprozess Spring Boot ähnlich zu machen, rufen Sie "ServerCodecConfigurer" mit "Autowired" auf Holen Sie sich "ListSE <HttpMessageReader <? >>" mit "ServerCodecConfigurer # getReaders ()", Der HttpMessageReader zum Parsen des HTTP-Anforderungshauptteils muss mit Stream # filter () usw. abgerufen werden.

Wenn Sie dies in "org.springframework.security.web.server.authentication.ServerAuthenticationConverter" von Spring Security schreiben, sieht es ungefähr so aus.

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-Typprüfung.
        if (contentType == null || acceptType.isCompatibleWith(contentType)) {
            return Mono.error(new AuthenticationException("Invalid Content-Type"));
        }

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

        //JsonParameter ist der Benutzername,Eine Klasse, die nur das Passwort im Feld speichert.
        //JsonAuthentication ist eine Unterklasse der Authentifizierung, in der nur JsonParameters gespeichert werden.
        //Die Definition wird für beide weggelassen.
        return this.httpMessageReaders.stream()
            // Content-Type: application/json fordert Daten an JsonParameter an.Kann in Klasse konvertiert werden
            //Suchen Sie eine HttpMessageReader-Implementierung.
            .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 +
                ")";
        }

    }
}

Um ehrlich zu sein, finde ich diese Implementierung ziemlich umständlich.

Asynchrone E / A zur Verarbeitung von Anforderungskörpern, die nicht wissen, wann sie gesendet werden, Da erst zur Laufzeit festgelegt wird, was für das Objekt verwendet wird, das den Nachrichtentext in Spring Boot verarbeitet Ich kann das verstehen

Selbst wenn Sie davon subtrahieren, dass es sich um eine eindeutige HTTP-Nachrichtenverarbeitung handelt, die sich von der Servert-API unterscheidet Dieses Verfahren jedes Mal zu schreiben schien sehr ärgerlich.

Da "Mono" verwendet wird, ist es etwas mühsam, eine praktische Dienstprogrammfunktion vorzubereiten.

Ich dachte, aber eines Tages bemerkte ich, dass die Funktion ServerRequest # create (ServerWebExchange, List <HttpMessageReader <? >>) existiert. Die von dieser Funktion generierte ServerRequest stammt aus der List <HttpMessageReader <? >> Give für die Verarbeitung des Nachrichtentexts. Es wird den entsprechenden "HttpMessageReader" abrufen und verwenden.

Sie möchten den Nachrichtentext konvertieren, indem Sie einfach "BodyExtractor" aus der in "BodyExtractors" bereitgestellten Funktion generieren.

Die Codemenge kann selbst mit dem vorherigen Code erheblich reduziert werden. Die Prüfung auf "Content-Type" bleibt nur für den Fall.

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 <? >>)" ist eine nützliche Funktion in "WebFilter". Nicht oft eingeführt.

Recommended Posts

Eine kurze Hand, die einen JSON-Body aus ServerHttpRequest mit einem selbst erstellten WebFilter usw. mit Spring WebFlux liest
So lesen Sie Request Body mit Spring Boot + Spring Security mehrmals
Sie verwenden den Kontext, um MDC mit Spring WebFlux zu verwenden
Wie man mit cli jdk etc. von oracle bekommt