【问题标题】:Webflux Spring 2.1.2 customize Content-TypeWebflux Spring 2.1.2 自定义 Content-Type
【发布时间】:2019-08-05 06:49:22
【问题描述】:

我正在尝试通过 WebClient 发布以获取 microsoft 令牌:

public WebClient getWebclient() {
    TcpClient client = TcpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
            .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(15)).addHandlerLast(new WriteTimeoutHandler(15)));

    ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(configurer -> {
                configurer.registerDefaults(true);
                FormHttpMessageReader formHttpMessageReader = new FormHttpMessageReader();
                formHttpMessageReader.setEnableLoggingRequestDetails(true);
                configurer.customCodecs().reader(formHttpMessageReader);
            })
            .build();

    return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(HttpClient.from(client).followRedirect(true)))
            .exchangeStrategies(strategies)
            .filter(logRequest())
            .filter(logResponse())
            .build();
}

MultiValueMap<String, String> credentials = new LinkedMultiValueMap<>();
        credentials.add("grant_type", "password");
        credentials.add("client_id", oauthClientId);
        credentials.add("resource", oauthResource);
        credentials.add("scope", oauthScope);
        credentials.add("username", oauthUsername);
        credentials.add("password", oauthPassword);

        Mono<MicrosoftToken> response = webClientService.getWebclient().post()
                .uri(oauthUrl)
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData(credentials))
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                        Mono.error(new WebClientException(clientResponse.bodyToMono(String.class), clientResponse.statusCode())))
                .bodyToMono(MicrosoftToken.class);

        this.cachedToken = response.block();

问题在于,微软无法处理 Content-type: application/x-www-form-urlencoded;charset=UTF-8

Spring 会自动将 charset=UTF-8 添加到请求中。我需要摆脱这个额外的字符集。我需要一个 Content-Type: application/x-www-form-urlencoded。这可能吗?否则我需要将我的 spring 版本降级到 2.0.0,其中不会自动添加字符集。

我的调试日志打印:

2019-03-14 10:08:42 DEBUG [reactor.netty.channel.ChannelOperationsHandler]: 
[id: 0x5d6effce, L:/192.168.148.14:52285 - 
R:login.microsoftonline.de/51.4.136.42:443] Writing object 
DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
POST /common/oauth2/token HTTP/1.1
user-agent: ReactorNetty/0.8.4.RELEASE
host: login.microsoftonline.de
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 205
2019-03-14 10:08:42 DEBUG [reactor.netty.channel.ChannelOperationsHandler]: 
[id: 0x5d6effce, L:/192.168.148.14:52285 - 
R:login.microsoftonline.de/51.4.136.42:443] Writing object 

我用 spring 版本 2.0.0 对此进行了测试,并且没有像在新版本中那样添加字符集:

POST /common/oauth2/token HTTP/1.1
user-agent: ReactorNetty/0.7.5.RELEASE
host: login.microsoftonline.de
accept-encoding: gzip
Content-Type: application/x-www-form-urlencoded
Content-Length: 205

【问题讨论】:

  • 您是否尝试过在使用 WebClient 发送请求时设置内容类型?你能显示一个实际发送请求的代码 sn-p 吗?
  • 是的,我试过了,我正在设置内容类型。但是 Spring 稍后会覆盖内容类型,或者更确切地说,将字符集添加到设置的内容类型中。

标签: spring spring-webflux


【解决方案1】:

我花了一个上午的时间才找到答案,但我终于成功了。问题是 Webflux BodyInserters.fromFormData 总是 将内容类型设置为 application/x-www-form-urlencoded;charset=... 而不管你在标题中设置了什么。

要解决这个问题,首先定义这个方法:

/**
 * This method is unfortunately necessary because of Spring Webflux's propensity to add {@code ";charset=..."}
 * to the {@code Content-Type} header, which the Generic Chinese Device doesn't handle properly.
 *
 * @return a {@link FormInserter} that doesn't add the character set to the content type header
 */
private FormInserter<String> formInserter() {

    return new FormInserter<String>() {

        private final MultiValueMap<String, String> data = new LinkedMultiValueMap<>();

        @Override public FormInserter<String> with(final String key, final String value) {
            data.add(key, value);
            return this;
        }

        @Override public FormInserter<String> with(final MultiValueMap<String, String> values) {
            data.addAll(values);
            return this;
        }

        @Override public Mono<Void> insert(final ClientHttpRequest outputMessage, final Context context) {
            final ResolvableType formDataType =
                    ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
            return new FormHttpMessageWriter() {
                @Override protected MediaType getMediaType(final MediaType mediaType) {
                    if (MediaType.APPLICATION_FORM_URLENCODED.equals(mediaType)) {
                        return mediaType;
                    } else {
                        return super.getMediaType(mediaType);
                    }
                }
            }.write(Mono.just(this.data), formDataType,
                    MediaType.APPLICATION_FORM_URLENCODED,
                    outputMessage,
                    context.hints());
        }
    };
}

然后,要调用您的网络服务,请执行以下操作:

final SomeResponseObject response = WebClient
        .builder()
        .build()
        .post()
        .uri(someOrOtherUri)
        .body(formInserter().with("param1", "value1")
                            .with("param2", "value2")
        )
        .retrieve()
        .bodyToFlux(SomeReponseObject.class)
        .blockLast();

请注意,上面的块主要用于演示目的。您可能希望也可能不想阻止并等待响应。

【讨论】:

  • 感谢分享!也解决了问题。
【解决方案2】:

这里有两种方法:

webClient
   .mutate()
   .defaultHeaders(headers -> {
      headers.add("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()
   }).build()
   . uri(uri)
   ...

OR 

webClient
   .post()
   .uri(uri)
   .body(body)
   .headers(headers -> getHttpHeaders())
   ...

private HttpHeaders getHttpHeaders(){
   HttpHeaders headers = new HttpHeaders();
   headers.add("Content-Type", "application/x-www-form-urlencoded")
   return headers;
}

您可以通过几种方式在 .headers 或 .defaultHeaders.. 中使用标头使用者。

但老实说,我认为字符集不是问题。如果您在响应中收到 application/json,可能是因为 Microsoft 通过您在应用注册中指定的重定向 url 转发带有该标头的请求。

好消息是这可能是可取的,因为 Microsoft 将令牌字段作为 json 返回,这允许您调用 .bodyToMono(MicrosoftToken)。我记得 BodyInserters.fromFormData 有问题,因为它实际上并没有对 MultiValueMap 中的值进行编码。

这是我正在使用的:

    private BodyInserter<String, ReactiveHttpOutputMessage> getBodyInserter(Map<String,String> parameters) {

        credentials.add("grant_type", encode("password"));
        credentials.add("client_id", encode(oauthClientId));
        credentials.add("resource", encode(oauthResource));
        // and so on..
        // note that parameters is a regular Map - not a MultiValueMap

        BodyInserter<String, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromObject(
                parameters.entrySet().stream()
                        .map(entry -> entry.getKey().concat("=").concat(entry.getValue()))
                        .collect(Collectors.joining("&", "", "")));

        return bodyInserter;
    }

    private String encode(String str) {
        try {
            return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            log.error("Error encoding req body", e);
        }
    }

【讨论】:

  • 除非这不起作用。检查我的答案以获得正确的解决方案。
猜你喜欢
  • 2011-04-24
  • 1970-01-01
  • 2019-03-29
  • 2011-09-05
  • 2018-07-09
  • 1970-01-01
  • 2020-06-09
  • 2012-01-21
  • 1970-01-01
相关资源
最近更新 更多