Cookie management for Webflux WebClient
Asked Answered
C

4

13

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();
Cedillo answered 7/4, 2019 at 9:47 Comment(1)
In short, I used ResponseCookie.getName() for the key and ResponseCookie.getValue() for the key’s value in the MultiValueMap of the post(). I will post the code next week once I figure out how the expected expiry time of the cookie could be anticipated and handled.Cedillo
C
6

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();

Cedillo answered 14/4, 2019 at 4:48 Comment(2)
Has anybody find out a better approach to deal with cookies in this matter?Pouliot
Nonetheless, the exchange() has already been marked as deprecated.Acadia
A
4

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())));
}
Arboretum answered 23/8, 2021 at 11:46 Comment(0)
P
1

The blocking (not recommended though) way could be:

MultiValueMap<String, String> myCookies = new LinkedMultiValueMap<>();

MyPojo body = webClient
        .post()
        .uri(myUrl)
        .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
        .bodyValue(myRequestBody)
        .exchangeToMono(response -> {
            if (response.statusCode().is2xxSuccessful()) {
                response.cookies().forEach((key, respCookies) -> cookies.add(key, respCookies.get(0).getValue()));
                return response.bodyToMono(MyPojo.class);
            } else {
                return response.createException().flatMap(e -> Mono.error(new MyException("Something went wrong")));
            }
        })
        .block();

You can grab the ResponseCookies from the ClientResponse if the status is successful, else, throw an exception, or handle it in the required manner.

And, then for the following request you can use the myCookies map:

webClient
  .get()
  .uri(userUrl)
  .cookies(cookieMap -> cookieMap.addAll(cookies))
// ...
Postconsonantal answered 5/9, 2023 at 7:39 Comment(0)
G
-1

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();
    }
Grocer answered 18/4, 2022 at 10:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.