'Cookie Management For Webflux WebClient

I have a WebClient that sends a JSON object with login credentials to a remote server. The remote server then returns the cookie. After which I need to POST data to that remote server along with the cookie. However, I cannot work out how re-use the cookie within the POST.

As far as I can tell, the login response gives the following structure MultiValueMap<String, ResponseCookie>, however the code to set the cookie on the POST requires MultiValueMap<String, String> or just cookie(String, String).

I assume that I must be missing some converter magic, but what? Do I even need return the whole cookie?

The cookie looks like this;

{SSO_Sticky_Session-47873-loadBalancedAdminGrp=[SSO_Sticky_Session-47873-loadBalancedAdminGrp=BNAMAKAKJABP; Path=/; HttpOnly], AUTH_TOKEN=[AUTH_TOKEN=v0l3baVZejIKjdzA1KGpkz4ccnosE6rKLQig1D2bdb-voFmVrF_aaYgzWl3Yc8QK; Path=/], uid=[uid=sjzipQdBtU30OlVbPWtDK2625i24i6t6g3Rjl5y5XcI=; Path=/], __cfduid=[__cfduid=dd872f39fd1d3bfe2a5c7316cd9ff63cd1554623603; Path=/; Domain=.aDomain.net; Max-Age=31535999; Expires=Mon, 6 Apr 2020 07:53:23 GMT; HttpOnly], JSESSIONID=[JSESSIONID=A264A713AD060EE12DA8215AEF66A3C0; Path=/aPath/; HttpOnly]}

My code is below. I have removed content type for brevity;

WebClient webClient = WebClient.create("https://remoteServer");
MultiValueMap<String, ResponseCookie> myCookies;

webClient
  .post()
  .uri("uri/login")
  .body(Mono.just(myLoginObject), MyLogin.class)
  .exchange()
  .subscribe(r -> 
    System.err.println("Received:" + r.cookies());
    myCookies = r.cookies();
   );

webClient
  .post()
  .uri("/uri/data")
  .cookies(????) // what goes here ??
  .body(....)
  .exchange();


Solution 1:[1]

Having written server side Java and JSP for a number of years, I had largely ignored the concept of cookies, as management is taken care of by (for example) Tomcat on the server side and by the browser on the client side. Any search for cookie handling in Spring always focused on the Spring server and rarely on Spring actually being a client of another server. Any examples for WebClient were simplistic and didn't assume any form of security negotiation.

Having read a cookie explanation Wikipedia Cookies and the cookie standard RFC6265, it made sense to me why the incoming cookie is in class ResponseCookie and the outgoing cookie was a String. The incoming cookie has additional meta-data on (for example) Domain, Path and Max-Age.

For my implementation, the vendor didn't specify which cookies needed to be returned, so I ended up returning all of them. Therefore, my amended code is as follows;

WebClient webClient = WebClient.create("https://remoteServer");
MultiValueMap<String, String> myCookies = new LinkedMultiValueMap<String, String>()

webClient
  .post()
  .uri("uri/login")
  .body(Mono.just(myLoginObject), MyLogin.class)
  .exchange()
  .subscribe(r -> 
      for (String key: r.cookies().keySet()) {
        myCookies.put(key, Arrays.asList(r.cookies().get(key).get(0).getValue()));
      }
   );

webClient
  .post()
  .uri("/uri/data")
  .cookies(cookies -> cookies.addAll(myCookies))
  .body(....)
  .exchange();

Solution 2:[2]

Since .exchange() has been deprecated but this thread comes up on popular search machines, let me add a code example using .exchangeToMono() below for future references.

Please note that I use an ExchangeFilterFunction which will send the authorization request before each request sent by the webClient bean:

@Bean("webClient")
public WebClient webClient(ReactorResourceFactory resourceFactory,
    ExchangeFilterFunction authFilter) {
    var httpClient = HttpClient.create(resourceFactory.getConnectionProvider());
    var clientHttpConnector = new ReactorClientHttpConnector(httpClient);
    return WebClient.builder().filter(authFilter).clientConnector(clientHttpConnector)
        .build();
}

@Bean("authWebClient")
public WebClient authWebClient(ReactorResourceFactory resourceFactory) {
    var httpClient = HttpClient.create(resourceFactory.getConnectionProvider());
    var clientHttpConnector = new ReactorClientHttpConnector(httpClient);
    return WebClient.builder().clientConnector(clientHttpConnector).build();
}

@Bean
public ExchangeFilterFunction authFilter(@Qualifier("authWebClient") WebClient authWebClient,
    @Value("${application.url:''}") String url,
    @Value("${application.basic-auth-credentials:''}") String basicAuthCredentials) {
return (request, next) -> authWebClient.get()
    .uri(url)
    .header("Authorization", String.format("Basic %s", basicAuthCredentials))
    .exchangeToMono(response -> next.exchange(ClientRequest.from(request)
        .headers(headers -> {
            headers.add("Authorization", String.format("Basic %s", basicAuthCredentials));
        })
        .cookies(readCookies(response))
        .build()));
}

private Consumer<MultiValueMap<String, String>> readCookies(ClientResponse response) {
return cookies -> response.cookies().forEach((responseCookieName, responseCookies) ->
    cookies.addAll(responseCookieName,
        responseCookies.stream().map(responseCookie -> responseCookie.getValue())
            .collect(Collectors.toList())));
}

Solution 3:[3]

This answer was inspired by @J. S..

.exchange should not be used anymore. Instead there is a new method called .exchangeToMono. With the help of Lambda one can edit and transform the response. The cookies can also be extracted. In this example the cookies are displayed in the console. But they can also be saved without problems.

    public String getWebsiteWithCookies() {
        var webClient = WebClient.create();

        return webClient.post()
                .uri("url")
                // Your headers & so here
                .exchangeToMono(response -> {
                    MultiValueMap<String, ResponseCookie> cookies = response.cookies();
                    for (var cookie : cookies.entrySet()) {
                        System.out.println(cookie.getKey() + " : " + cookie.getValue());
                    }

                    return response.bodyToMono(String.class);
                })
                .block();
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Community
Solution 2 J. S.
Solution 3