diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java index 13d9adbc45c..0254d5a4358 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java @@ -150,6 +150,12 @@ public final class ResponseCookie extends HttpCookie { return this; } + @Override + public ResponseCookieBuilder maxAge(long maxAgeSeconds) { + this.maxAge = maxAgeSeconds >= 0 ? Duration.ofSeconds(maxAgeSeconds) : Duration.ofSeconds(-1); + return this; + } + @Override public ResponseCookieBuilder domain(String domain) { this.domain = domain; @@ -163,14 +169,14 @@ public final class ResponseCookie extends HttpCookie { } @Override - public ResponseCookieBuilder secure() { - this.secure = true; + public ResponseCookieBuilder secure(boolean secure) { + this.secure = secure; return this; } @Override - public ResponseCookieBuilder httpOnly() { - this.httpOnly = true; + public ResponseCookieBuilder httpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; return this; } @@ -197,6 +203,11 @@ public final class ResponseCookie extends HttpCookie { */ ResponseCookieBuilder maxAge(Duration maxAge); + /** + * Set the cookie "Max-Age" attribute in seconds. + */ + ResponseCookieBuilder maxAge(long maxAgeSeconds); + /** * Set the cookie "Path" attribute. */ @@ -210,13 +221,13 @@ public final class ResponseCookie extends HttpCookie { /** * Add the "Secure" attribute to the cookie. */ - ResponseCookieBuilder secure(); + ResponseCookieBuilder secure(boolean secure); /** * Add the "HttpOnly" attribute to the cookie. * @see http://www.owasp.org/index.php/HTTPOnly */ - ResponseCookieBuilder httpOnly(); + ResponseCookieBuilder httpOnly(boolean httpOnly); /** * Create the HttpCookie. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index 850eea9e36e..8018698e769 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -23,8 +23,12 @@ import java.util.function.Supplier; import reactor.core.publisher.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Base class for {@link ClientHttpRequest} implementations. @@ -36,6 +40,8 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { private final HttpHeaders headers; + private final MultiValueMap cookies; + private AtomicReference state = new AtomicReference<>(State.NEW); private final List>> beforeCommitActions = new ArrayList<>(4); @@ -47,6 +53,7 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { else { this.headers = httpHeaders; } + this.cookies = new LinkedMultiValueMap<>(); } @Override @@ -57,6 +64,14 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { return this.headers; } + @Override + public MultiValueMap getCookies() { + if (State.COMITTED.equals(this.state.get())) { + return CollectionUtils.unmodifiableMultiValueMap(this.cookies); + } + return this.cookies; + } + protected Mono applyBeforeCommit() { Mono mono = Mono.empty(); if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java index 32c38940aef..3e7098a0434 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -20,8 +20,10 @@ import java.net.URI; import reactor.core.publisher.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpMethod; import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.util.MultiValueMap; /** * Represents a reactive client-side HTTP request. @@ -41,6 +43,11 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ URI getURI(); + /** + * Return a mutable map of request cookies to send to the server. + */ + MultiValueMap getCookies(); + /** * Execute this request, resulting in a reactive stream of a single * {@link org.springframework.http.client.ClientHttpResponse}. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java index 34cad69c105..502102b2279 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java @@ -18,6 +18,8 @@ package org.springframework.http.client.reactive; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ResponseCookie; +import org.springframework.util.MultiValueMap; /** * Represents a reactive client-side HTTP response. @@ -31,4 +33,9 @@ public interface ClientHttpResponse extends ReactiveHttpInputMessage { */ HttpStatus getStatusCode(); + /** + * Return a read-only map of response cookies received from the server. + */ + MultiValueMap getCookies(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 81ec72a3d41..5ff05bc20d1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -17,16 +17,19 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Collection; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpClient; +import reactor.io.netty.http.model.Cookie; import reactor.io.netty.http.model.Method; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -96,13 +99,13 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { channel.headers().removeTransferEncodingChunked(); } return applyBeforeCommit() - .after(() -> - { - getHeaders().entrySet().stream() - .forEach(e -> channel.headers().set(e.getKey(), e.getValue())); - return Mono.empty(); - } - ) + .after(() -> { + getHeaders().entrySet().stream().forEach(e -> + channel.headers().set(e.getKey(), e.getValue())); + getCookies().values().stream().flatMap(Collection::stream).forEach(cookie -> + channel.addCookie(cookie.getName(), new ReactorCookie(cookie))); + return Mono.empty(); + }) .after(() -> { if (body != null) { return channel.writeBufferWith(body); @@ -115,5 +118,29 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); } + + /** + * At present Reactor does not provide a {@link Cookie} implementation. + */ + private final static class ReactorCookie extends Cookie { + + private final HttpCookie httpCookie; + + + public ReactorCookie(HttpCookie httpCookie) { + this.httpCookie = httpCookie; + } + + @Override + public String name() { + return this.httpCookie.getName(); + } + + @Override + public String value() { + return this.httpCookie.getValue(); + } + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 937d26d07b5..df2d58aded3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -17,6 +17,7 @@ package org.springframework.http.client.reactive; import java.nio.ByteBuffer; +import java.util.Collection; import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; @@ -26,6 +27,10 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * {@link ClientHttpResponse} implementation for the Reactor Net HTTP client @@ -62,10 +67,27 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { return HttpStatus.valueOf(this.channel.responseStatus().getCode()); } + @Override + public MultiValueMap getCookies() { + MultiValueMap result = new LinkedMultiValueMap<>(); + this.channel.cookies().values().stream().flatMap(Collection::stream) + .forEach(cookie -> { + ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value()) + .domain(cookie.domain()) + .path(cookie.path()) + .maxAge(cookie.maxAge()) + .secure(cookie.secure()) + .httpOnly(cookie.httpOnly()) + .build(); + result.add(cookie.name(), responseCookie); + }); + return CollectionUtils.unmodifiableMultiValueMap(result); + } + @Override public String toString() { return "ReactorClientHttpResponse{" + - "request=" + this.channel.method() + " " + this.channel.uri().toString() + "," + + "request=" + this.channel.method() + " " + this.channel.uri() + "," + "status=" + getStatusCode() + '}'; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index 74c894e7fa8..7ab96b37c66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -17,10 +17,12 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Map; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.client.HttpClient; import io.reactivex.netty.protocol.http.client.HttpClientRequest; import org.reactivestreams.Publisher; @@ -31,6 +33,7 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -102,6 +105,11 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { req = req.addHeader(entry.getKey(), value); } } + for (Map.Entry> entry : getCookies().entrySet()) { + for (HttpCookie cookie : entry.getValue()) { + req.addCookie(new DefaultCookie(cookie.getName(), cookie.getValue())); + } + } return req; }) .map(req -> { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java index a6a2efe3b2f..5edd02ef9e9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java @@ -16,6 +16,8 @@ package org.springframework.http.client.reactive; +import java.util.Collection; + import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.client.HttpClientResponse; import reactor.core.converter.RxJava1ObservableConverter; @@ -25,7 +27,11 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * {@link ClientHttpResponse} implementation for the RxNetty HTTP client @@ -38,8 +44,11 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { private final HttpHeaders headers; + private final MultiValueMap cookies; + private final NettyDataBufferAllocator allocator; + public RxNettyClientHttpResponse(HttpClientResponse response, NettyDataBufferAllocator allocator) { Assert.notNull("'request', request must not be null"); @@ -48,8 +57,26 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { this.response = response; this.headers = new HttpHeaders(); this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey(), e.getValue())); + this.cookies = initCookies(response); + } + + private static MultiValueMap initCookies(HttpClientResponse response) { + MultiValueMap result = new LinkedMultiValueMap<>(); + response.getCookies().values().stream().flatMap(Collection::stream) + .forEach(cookie -> { + ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value()) + .domain(cookie.domain()) + .path(cookie.path()) + .maxAge(cookie.maxAge()) + .secure(cookie.isSecure()) + .httpOnly(cookie.isHttpOnly()) + .build(); + result.add(cookie.name(), responseCookie); + }); + return CollectionUtils.unmodifiableMultiValueMap(result); } + @Override public HttpStatus getStatusCode() { return HttpStatus.valueOf(this.response.getStatus().code()); @@ -64,4 +91,10 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { public HttpHeaders getHeaders() { return this.headers; } + + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 18ba0f172b7..4e641499f55 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -37,7 +37,7 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { void setStatusCode(HttpStatus status); /** - * Return a mutable map with cookies to be sent to the client. + * Return a mutable map with the cookies to send to the server. */ MultiValueMap getCookies(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index db9e79a3c04..d39d60a45cb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -103,7 +103,7 @@ public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests this.requestCookies.size(); // Cause lazy loading response.getCookies().add("SID", ResponseCookie.from("SID", "31d4d96e407aad42") - .path("/").secure().httpOnly().build()); + .path("/").secure(true).httpOnly(true).build()); response.getCookies().add("lang", ResponseCookie.from("lang", "en-US") .domain("example.com").path("/").build());