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. Get
ListSE <HttpMessageReader <? >>with
ServerCodecConfigurer # 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.