Set sameSite in ClientHttpResponse implementations
Closes gh-25785
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user