Set sameSite in ClientHttpResponse implementations

Closes gh-25785
This commit is contained in:
Rossen Stoyanchev
2020-09-22 07:33:03 +01:00
parent 87399aedf7
commit 1061bcdba2
4 changed files with 61 additions and 28 deletions

View File

@@ -81,13 +81,15 @@ class HttpComponentsClientHttpResponse implements ClientHttpResponse {
public MultiValueMap<String, ResponseCookie> getCookies() { public MultiValueMap<String, ResponseCookie> getCookies() {
LinkedMultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>(); LinkedMultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
this.context.getCookieStore().getCookies().forEach(cookie -> this.context.getCookieStore().getCookies().forEach(cookie ->
result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) result.add(cookie.getName(),
.domain(cookie.getDomain()) ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
.path(cookie.getPath()) .domain(cookie.getDomain())
.maxAge(getMaxAgeSeconds(cookie)) .path(cookie.getPath())
.secure(cookie.isSecure()) .maxAge(getMaxAgeSeconds(cookie))
.httpOnly(cookie.containsAttribute("httponly")) .secure(cookie.isSecure())
.build())); .httpOnly(cookie.containsAttribute("httponly"))
.sameSite(cookie.getAttribute("samesite"))
.build()));
return result; return result;
} }

View File

@@ -18,6 +18,8 @@ package org.springframework.http.client.reactive;
import java.net.HttpCookie; import java.net.HttpCookie;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.reactive.client.ReactiveResponse; import org.eclipse.jetty.reactive.client.ReactiveResponse;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
@@ -27,6 +29,7 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@@ -41,6 +44,9 @@ import org.springframework.util.MultiValueMap;
*/ */
class JettyClientHttpResponse implements ClientHttpResponse { class JettyClientHttpResponse implements ClientHttpResponse {
private static final Pattern SAMESITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
private final ReactiveResponse reactiveResponse; private final ReactiveResponse reactiveResponse;
private final Flux<DataBuffer> content; private final Flux<DataBuffer> content;
@@ -72,19 +78,28 @@ class JettyClientHttpResponse implements ClientHttpResponse {
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>(); MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
List<String> cookieHeader = getHeaders().get(HttpHeaders.SET_COOKIE); List<String> cookieHeader = getHeaders().get(HttpHeaders.SET_COOKIE);
if (cookieHeader != null) { if (cookieHeader != null) {
cookieHeader.forEach(header -> HttpCookie.parse(header) cookieHeader.forEach(header ->
.forEach(c -> result.add(c.getName(), ResponseCookie.fromClientResponse(c.getName(), c.getValue()) HttpCookie.parse(header).forEach(cookie -> result.add(cookie.getName(),
.domain(c.getDomain()) ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
.path(c.getPath()) .domain(cookie.getDomain())
.maxAge(c.getMaxAge()) .path(cookie.getPath())
.secure(c.getSecure()) .maxAge(cookie.getMaxAge())
.httpOnly(c.isHttpOnly()) .secure(cookie.getSecure())
.build())) .httpOnly(cookie.isHttpOnly())
.sameSite(parseSameSite(header))
.build()))
); );
} }
return CollectionUtils.unmodifiableMultiValueMap(result); 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 @Override
public Flux<DataBuffer> getBody() { public Flux<DataBuffer> getBody() {
return this.content; return this.content;

View File

@@ -21,6 +21,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import io.netty.buffer.ByteBufAllocator; 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.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@@ -34,6 +36,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@@ -129,17 +132,31 @@ class ReactorClientHttpResponse implements ClientHttpResponse {
@Override @Override
public MultiValueMap<String, ResponseCookie> getCookies() { public MultiValueMap<String, ResponseCookie> getCookies() {
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>(); MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
this.response.cookies().values().stream().flatMap(Collection::stream) this.response.cookies().values().stream()
.forEach(c -> result.add(c.name(), ResponseCookie.fromClientResponse(c.name(), c.value()) .flatMap(Collection::stream)
.domain(c.domain()) .forEach(cookie -> result.add(cookie.name(),
.path(c.path()) ResponseCookie.fromClientResponse(cookie.name(), cookie.value())
.maxAge(c.maxAge()) .domain(cookie.domain())
.secure(c.isSecure()) .path(cookie.path())
.httpOnly(c.isHttpOnly()) .maxAge(cookie.maxAge())
.build())); .secure(cookie.isSecure())
.httpOnly(cookie.isHttpOnly())
.sameSite(getSameSite(cookie))
.build()));
return CollectionUtils.unmodifiableMultiValueMap(result); 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 * Called by {@link ReactorClientHttpConnector} when a cancellation is detected
* but the content has not been subscribed to. If the subscription never * but the content has not been subscribed to. If the subscription never

View File

@@ -120,10 +120,8 @@ class WebClientIntegrationTests {
void retrieve(ClientHttpConnector connector) { void retrieve(ClientHttpConnector connector) {
startServer(connector); startServer(connector);
prepareResponse(response -> response.setHeader("Content-Type", "text/plain") prepareResponse(response ->
.addHeader("Set-Cookie", "testkey1=testvalue1;") response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
.addHeader("Set-Cookie", "testkey2=testvalue2; Max-Age=42; HttpOnly; Secure")
.setBody("Hello Spring!"));
Mono<String> result = this.webClient.get() Mono<String> result = this.webClient.get()
.uri("/greeting") .uri("/greeting")
@@ -1102,7 +1100,7 @@ class WebClientIntegrationTests {
prepareResponse(response -> response prepareResponse(response -> response
.setHeader("Content-Type", "text/plain") .setHeader("Content-Type", "text/plain")
.addHeader("Set-Cookie", "testkey1=testvalue1;") .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")); .setBody("test"));
Mono<ClientResponse> result = this.webClient.get() Mono<ClientResponse> result = this.webClient.get()
@@ -1123,6 +1121,7 @@ class WebClientIntegrationTests {
assertThat(cookie2.getValue()).isEqualTo("testvalue2"); assertThat(cookie2.getValue()).isEqualTo("testvalue2");
assertThat(cookie2.isSecure()).isTrue(); assertThat(cookie2.isSecure()).isTrue();
assertThat(cookie2.isHttpOnly()).isTrue(); assertThat(cookie2.isHttpOnly()).isTrue();
assertThat(cookie2.getSameSite()).isEqualTo("Lax");
assertThat(cookie2.getMaxAge().getSeconds()).isEqualTo(42); assertThat(cookie2.getMaxAge().getSeconds()).isEqualTo(42);
}) })
.expectComplete() .expectComplete()