Set sameSite in ClientHttpResponse implementations
Closes gh-25785
This commit is contained in:
parent
87399aedf7
commit
1061bcdba2
|
|
@ -81,13 +81,15 @@ class HttpComponentsClientHttpResponse implements ClientHttpResponse {
|
|||
public MultiValueMap<String, ResponseCookie> getCookies() {
|
||||
LinkedMultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
|
||||
this.context.getCookieStore().getCookies().forEach(cookie ->
|
||||
result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.path(cookie.getPath())
|
||||
.maxAge(getMaxAgeSeconds(cookie))
|
||||
.secure(cookie.isSecure())
|
||||
.httpOnly(cookie.containsAttribute("httponly"))
|
||||
.build()));
|
||||
result.add(cookie.getName(),
|
||||
ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.path(cookie.getPath())
|
||||
.maxAge(getMaxAgeSeconds(cookie))
|
||||
.secure(cookie.isSecure())
|
||||
.httpOnly(cookie.containsAttribute("httponly"))
|
||||
.sameSite(cookie.getAttribute("samesite"))
|
||||
.build()));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package org.springframework.http.client.reactive;
|
|||
|
||||
import java.net.HttpCookie;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jetty.reactive.client.ReactiveResponse;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
|
@ -27,6 +29,7 @@ import org.springframework.core.io.buffer.DataBuffer;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
|
@ -41,6 +44,9 @@ import org.springframework.util.MultiValueMap;
|
|||
*/
|
||||
class JettyClientHttpResponse implements ClientHttpResponse {
|
||||
|
||||
private static final Pattern SAMESITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
|
||||
|
||||
private final ReactiveResponse reactiveResponse;
|
||||
|
||||
private final Flux<DataBuffer> content;
|
||||
|
|
@ -72,19 +78,28 @@ class JettyClientHttpResponse implements ClientHttpResponse {
|
|||
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
|
||||
List<String> cookieHeader = getHeaders().get(HttpHeaders.SET_COOKIE);
|
||||
if (cookieHeader != null) {
|
||||
cookieHeader.forEach(header -> HttpCookie.parse(header)
|
||||
.forEach(c -> result.add(c.getName(), ResponseCookie.fromClientResponse(c.getName(), c.getValue())
|
||||
.domain(c.getDomain())
|
||||
.path(c.getPath())
|
||||
.maxAge(c.getMaxAge())
|
||||
.secure(c.getSecure())
|
||||
.httpOnly(c.isHttpOnly())
|
||||
.build()))
|
||||
cookieHeader.forEach(header ->
|
||||
HttpCookie.parse(header).forEach(cookie -> result.add(cookie.getName(),
|
||||
ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.path(cookie.getPath())
|
||||
.maxAge(cookie.getMaxAge())
|
||||
.secure(cookie.getSecure())
|
||||
.httpOnly(cookie.isHttpOnly())
|
||||
.sameSite(parseSameSite(header))
|
||||
.build()))
|
||||
);
|
||||
}
|
||||
return CollectionUtils.unmodifiableMultiValueMap(result);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String parseSameSite(String headerValue) {
|
||||
Matcher matcher = SAMESITE_PATTERN.matcher(headerValue);
|
||||
return (matcher.matches() ? matcher.group(1) : null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
return this.content;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import java.util.function.BiFunction;
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.handler.codec.http.cookie.Cookie;
|
||||
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
|
@ -34,6 +36,7 @@ import org.springframework.http.HttpHeaders;
|
|||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
|
@ -129,17 +132,31 @@ class ReactorClientHttpResponse implements ClientHttpResponse {
|
|||
@Override
|
||||
public MultiValueMap<String, ResponseCookie> getCookies() {
|
||||
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
|
||||
this.response.cookies().values().stream().flatMap(Collection::stream)
|
||||
.forEach(c -> result.add(c.name(), ResponseCookie.fromClientResponse(c.name(), c.value())
|
||||
.domain(c.domain())
|
||||
.path(c.path())
|
||||
.maxAge(c.maxAge())
|
||||
.secure(c.isSecure())
|
||||
.httpOnly(c.isHttpOnly())
|
||||
.build()));
|
||||
this.response.cookies().values().stream()
|
||||
.flatMap(Collection::stream)
|
||||
.forEach(cookie -> result.add(cookie.name(),
|
||||
ResponseCookie.fromClientResponse(cookie.name(), cookie.value())
|
||||
.domain(cookie.domain())
|
||||
.path(cookie.path())
|
||||
.maxAge(cookie.maxAge())
|
||||
.secure(cookie.isSecure())
|
||||
.httpOnly(cookie.isHttpOnly())
|
||||
.sameSite(getSameSite(cookie))
|
||||
.build()));
|
||||
return CollectionUtils.unmodifiableMultiValueMap(result);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getSameSite(Cookie cookie) {
|
||||
if (cookie instanceof DefaultCookie) {
|
||||
DefaultCookie defaultCookie = (DefaultCookie) cookie;
|
||||
if (defaultCookie.sameSite() != null) {
|
||||
return defaultCookie.sameSite().name();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link ReactorClientHttpConnector} when a cancellation is detected
|
||||
* but the content has not been subscribed to. If the subscription never
|
||||
|
|
|
|||
|
|
@ -120,10 +120,8 @@ class WebClientIntegrationTests {
|
|||
void retrieve(ClientHttpConnector connector) {
|
||||
startServer(connector);
|
||||
|
||||
prepareResponse(response -> response.setHeader("Content-Type", "text/plain")
|
||||
.addHeader("Set-Cookie", "testkey1=testvalue1;")
|
||||
.addHeader("Set-Cookie", "testkey2=testvalue2; Max-Age=42; HttpOnly; Secure")
|
||||
.setBody("Hello Spring!"));
|
||||
prepareResponse(response ->
|
||||
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
Mono<String> result = this.webClient.get()
|
||||
.uri("/greeting")
|
||||
|
|
@ -1102,7 +1100,7 @@ class WebClientIntegrationTests {
|
|||
prepareResponse(response -> response
|
||||
.setHeader("Content-Type", "text/plain")
|
||||
.addHeader("Set-Cookie", "testkey1=testvalue1;")
|
||||
.addHeader("Set-Cookie", "testkey2=testvalue2; Max-Age=42; HttpOnly; Secure")
|
||||
.addHeader("Set-Cookie", "testkey2=testvalue2; Max-Age=42; HttpOnly; SameSite=Lax; Secure")
|
||||
.setBody("test"));
|
||||
|
||||
Mono<ClientResponse> result = this.webClient.get()
|
||||
|
|
@ -1123,6 +1121,7 @@ class WebClientIntegrationTests {
|
|||
assertThat(cookie2.getValue()).isEqualTo("testvalue2");
|
||||
assertThat(cookie2.isSecure()).isTrue();
|
||||
assertThat(cookie2.isHttpOnly()).isTrue();
|
||||
assertThat(cookie2.getSameSite()).isEqualTo("Lax");
|
||||
assertThat(cookie2.getMaxAge().getSeconds()).isEqualTo(42);
|
||||
})
|
||||
.expectComplete()
|
||||
|
|
|
|||
Loading…
Reference in New Issue