Refactor HTTP Cookie support

There is now an HttpCookie (simple name-value pair) and separately a
ServerHttpCookie sub-class with additional attributes that a server
can send to clients.

HttpHeaders is no longer the place to access cookies. Instead there is
a read-only HttpCookie map on ServerHttpRequest and a mutable
ServerHttpCookie map on ServerHttpResponse.

Cookies are stored in a map that preserves their order.
This commit is contained in:
Rossen Stoyanchev
2016-03-03 21:30:06 -05:00
parent a8c777b35f
commit dbc6478b8a
20 changed files with 382 additions and 1519 deletions

View File

@@ -15,54 +15,28 @@
*/
package org.springframework.http;
import java.time.Duration;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Representation for an HTTP Cookie.
* Represents an HTTP Cookie with a name and value.
*
* <p>Use the {@link #clientCookie} factory method to create a client-to-server,
* name-value pair cookie and the {@link #serverCookie} factory method to build
* a server-to-client cookie with additional attributes.
* <p>The {@link ServerHttpCookie} sub-class exposes the extra attributes that
* a server can include in a Set-Cookie response header.
*
* @author Rossen Stoyanchev
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public final class HttpCookie {
public class HttpCookie {
private final String name;
private final String value;
private final Duration maxAge;
private final String domain;
private final String path;
private final boolean secure;
private final boolean httpOnly;
private HttpCookie(String name, String value) {
this(name, value, Duration.ofSeconds(-1), null, null, false, false);
}
private HttpCookie(String name, String value, Duration maxAge, String domain, String path,
boolean secure, boolean httpOnly) {
public HttpCookie(String name, String value) {
Assert.hasLength(name, "'name' is required and must not be empty.");
Assert.notNull(maxAge);
this.name = name;
this.value = (value != null ? value : "");
this.maxAge = maxAge;
this.domain = domain;
this.path = path;
this.secure = secure;
this.httpOnly = httpOnly;
}
/**
@@ -73,59 +47,16 @@ public final class HttpCookie {
}
/**
* Return the cookie value.
* Return the cookie value or an empty string, never {@code null}.
*/
public String getValue() {
return this.value;
}
/**
* Return the cookie "Max-Age" attribute in seconds.
*
* <p>A positive value indicates when the cookie expires relative to the
* current time. A value of 0 means the cookie should expire immediately.
* A negative value means no "Max-Age" attribute in which case the cookie
* is removed when the browser is closed.
*/
public Duration getMaxAge() {
return this.maxAge;
}
/**
* Return the cookie "Domain" attribute.
*/
public String getDomain() {
return this.domain;
}
/**
* Return the cookie "Path" attribute.
*/
public String getPath() {
return this.path;
}
/**
* Return {@code true} if the cookie has the "Secure" attribute.
*/
public boolean isSecure() {
return this.secure;
}
/**
* Return {@code true} if the cookie has the "HttpOnly" attribute.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
public boolean isHttpOnly() {
return this.httpOnly;
}
@Override
public int hashCode() {
int result = this.name.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain);
result = 31 * result + ObjectUtils.nullSafeHashCode(this.path);
return result;
return this.name.hashCode();
}
@Override
@@ -137,122 +68,7 @@ public final class HttpCookie {
return false;
}
HttpCookie otherCookie = (HttpCookie) other;
return (this.name.equalsIgnoreCase(otherCookie.getName()) &&
ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) &&
ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain()));
}
/**
* Factory method to create a cookie sent from a client to a server.
* Client cookies are name-value pairs only without attributes.
* @param name the cookie name
* @param value the cookie value
* @return the created cookie instance
*/
public static HttpCookie clientCookie(String name, String value) {
return new HttpCookie(name, value);
}
/**
* Factory method to obtain a builder for a server-defined cookie that starts
* with a name-value pair and may also include attributes.
* @param name the cookie name
* @param value the cookie value
* @return the created cookie instance
*/
public static HttpCookieBuilder serverCookie(final String name, final String value) {
return new HttpCookieBuilder() {
private Duration maxAge = Duration.ofSeconds(-1);
private String domain;
private String path;
private boolean secure;
private boolean httpOnly;
@Override
public HttpCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
return this;
}
@Override
public HttpCookieBuilder domain(String domain) {
this.domain = domain;
return this;
}
@Override
public HttpCookieBuilder path(String path) {
this.path = path;
return this;
}
@Override
public HttpCookieBuilder secure() {
this.secure = true;
return this;
}
@Override
public HttpCookieBuilder httpOnly() {
this.httpOnly = true;
return this;
}
@Override
public HttpCookie build() {
return new HttpCookie(name, value, this.maxAge, this.domain, this.path,
this.secure, this.httpOnly);
}
};
}
/**
* A builder for a server-defined HttpCookie with attributes.
*/
public interface HttpCookieBuilder {
/**
* Set the cookie "Max-Age" attribute.
*
* <p>A positive value indicates when the cookie should expire relative
* to the current time. A value of 0 means the cookie should expire
* immediately. A negative value results in no "Max-Age" attribute in
* which case the cookie is removed when the browser is closed.
*/
HttpCookieBuilder maxAge(Duration maxAge);
/**
* Set the cookie "Path" attribute.
*/
HttpCookieBuilder path(String path);
/**
* Set the cookie "Domain" attribute.
*/
HttpCookieBuilder domain(String domain);
/**
* Add the "Secure" attribute to the cookie.
*/
HttpCookieBuilder secure();
/**
* Add the "HttpOnly" attribute to the cookie.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
HttpCookieBuilder httpOnly();
/**
* Create the HttpCookie.
*/
HttpCookie build();
return (this.name.equalsIgnoreCase(otherCookie.getName()));
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.time.Duration;
import java.util.Optional;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Represents a server-side cookie with extra attributes that a server can
* include in a Set-Cookie response header.
*
* <p>Use {@link #with} to create a {@code ServerHttpCookie}.
*
* @author Rossen Stoyanchev
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public final class ServerHttpCookie extends HttpCookie {
private final Duration maxAge;
private final Optional<String> domain;
private final Optional<String> path;
private final boolean secure;
private final boolean httpOnly;
/**
* Private constructor. See {@link #with(String, String)}.
*/
private ServerHttpCookie(String name, String value, Duration maxAge, String domain,
String path, boolean secure, boolean httpOnly) {
super(name, value);
Assert.notNull(maxAge);
this.maxAge = maxAge;
this.domain = Optional.ofNullable(domain);
this.path = Optional.ofNullable(path);
this.secure = secure;
this.httpOnly = httpOnly;
}
/**
* Return the cookie "Max-Age" attribute in seconds.
*
* <p>A positive value indicates when the cookie expires relative to the
* current time. A value of 0 means the cookie should expire immediately.
* A negative value means no "Max-Age" attribute in which case the cookie
* is removed when the browser is closed.
*/
public Duration getMaxAge() {
return this.maxAge;
}
/**
* Return the cookie "Domain" attribute.
*/
public Optional<String> getDomain() {
return this.domain;
}
/**
* Return the cookie "Path" attribute.
*/
public Optional<String> getPath() {
return this.path;
}
/**
* Return {@code true} if the cookie has the "Secure" attribute.
*/
public boolean isSecure() {
return this.secure;
}
/**
* Return {@code true} if the cookie has the "HttpOnly" attribute.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
public boolean isHttpOnly() {
return this.httpOnly;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain);
result = 31 * result + ObjectUtils.nullSafeHashCode(this.path);
return result;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ServerHttpCookie)) {
return false;
}
ServerHttpCookie otherCookie = (ServerHttpCookie) other;
return (getName().equalsIgnoreCase(otherCookie.getName()) &&
ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) &&
ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain()));
}
/**
* Factory method to obtain a builder for a server-defined cookie that starts
* with a name-value pair and may also include attributes.
* @param name the cookie name
* @param value the cookie value
* @return the created cookie instance
*/
public static ServerHttpCookieBuilder with(final String name, final String value) {
return new ServerHttpCookieBuilder() {
private Duration maxAge = Duration.ofSeconds(-1);
private String domain;
private String path;
private boolean secure;
private boolean httpOnly;
@Override
public ServerHttpCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
return this;
}
@Override
public ServerHttpCookieBuilder domain(String domain) {
this.domain = domain;
return this;
}
@Override
public ServerHttpCookieBuilder path(String path) {
this.path = path;
return this;
}
@Override
public ServerHttpCookieBuilder secure() {
this.secure = true;
return this;
}
@Override
public ServerHttpCookieBuilder httpOnly() {
this.httpOnly = true;
return this;
}
@Override
public ServerHttpCookie build() {
return new ServerHttpCookie(name, value, this.maxAge, this.domain, this.path,
this.secure, this.httpOnly);
}
};
}
/**
* A builder for a server-defined HttpCookie with attributes.
*/
public interface ServerHttpCookieBuilder {
/**
* Set the cookie "Max-Age" attribute.
*
* <p>A positive value indicates when the cookie should expire relative
* to the current time. A value of 0 means the cookie should expire
* immediately. A negative value results in no "Max-Age" attribute in
* which case the cookie is removed when the browser is closed.
*/
ServerHttpCookieBuilder maxAge(Duration maxAge);
/**
* Set the cookie "Path" attribute.
*/
ServerHttpCookieBuilder path(String path);
/**
* Set the cookie "Domain" attribute.
*/
ServerHttpCookieBuilder domain(String domain);
/**
* Add the "Secure" attribute to the cookie.
*/
ServerHttpCookieBuilder secure();
/**
* Add the "HttpOnly" attribute to the cookie.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
ServerHttpCookieBuilder httpOnly();
/**
* Create the HttpCookie.
*/
ServerHttpCookie build();
}
}

View File

@@ -17,14 +17,14 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Common base class for {@link ServerHttpRequest} implementations.
@@ -37,6 +37,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
private HttpHeaders headers;
private MultiValueMap<String, HttpCookie> cookies;
@Override
public URI getURI() {
@@ -61,7 +63,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
@Override
public HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = new HttpHeaders(new HttpCookieInputMap());
this.headers = new HttpHeaders();
initHeaders(this.headers);
}
return this.headers;
@@ -74,89 +76,20 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
*/
protected abstract void initHeaders(HttpHeaders headers);
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
if (this.cookies == null) {
this.cookies = new LinkedMultiValueMap<String, HttpCookie>();
initCookies(this.cookies);
}
return this.cookies;
}
/**
* Initialize the cookies from the underlying request. Invoked lazily on the
* first access to cookies via {@link #getHeaders()} and then cached.
* @param cookies the map to add cookies to
*/
protected abstract void initCookies(Map<String, List<HttpCookie>> cookies);
/**
* Read-only map of input cookies with lazy initialization.
*/
private class HttpCookieInputMap implements Map<String, List<HttpCookie>> {
private Map<String, List<HttpCookie>> cookies;
private Map<String, List<HttpCookie>> getCookies() {
if (this.cookies == null) {
this.cookies = new LinkedCaseInsensitiveMap<>();
initCookies(this.cookies);
}
return this.cookies;
}
@Override
public int size() {
return getCookies().size();
}
@Override
public boolean isEmpty() {
return getCookies().isEmpty();
}
@Override
public boolean containsKey(Object key) {
return getCookies().containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return getCookies().containsValue(value);
}
@Override
public List<HttpCookie> get(Object key) {
return getCookies().get(key);
}
@Override
public Set<String> keySet() {
return getCookies().keySet();
}
@Override
public Collection<List<HttpCookie>> values() {
return getCookies().values();
}
@Override
public Set<Entry<String, List<HttpCookie>>> entrySet() {
return getCookies().entrySet();
}
@Override
public List<HttpCookie> put(String key, List<HttpCookie> value) {
throw new UnsupportedOperationException("Can't modify client sent cookies.");
}
@Override
public List<HttpCookie> remove(Object key) {
throw new UnsupportedOperationException("Can't modify client sent cookies.");
}
@Override
public void putAll(Map<? extends String, ? extends List<HttpCookie>> map) {
throw new UnsupportedOperationException("Can't modify client sent cookies.");
}
@Override
public void clear() {
throw new UnsupportedOperationException("Can't modify client sent cookies.");
}
}
protected abstract void initCookies(MultiValueMap<String, HttpCookie> cookies);
}

View File

@@ -17,6 +17,7 @@ package org.springframework.http.server.reactive;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@@ -24,8 +25,13 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
@@ -37,6 +43,8 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
private final HttpHeaders headers;
private final MultiValueMap<String, ServerHttpCookie> cookies;
private AtomicReference<State> state = new AtomicReference<>(State.NEW);
private final List<Supplier<? extends Mono<Void>>> beforeCommitActions = new ArrayList<>(4);
@@ -44,6 +52,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
protected AbstractServerHttpResponse() {
this.headers = new HttpHeaders();
this.cookies = new LinkedMultiValueMap<String, ServerHttpCookie>();
}
@@ -55,6 +64,14 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
return this.headers;
}
@Override
public MultiValueMap<String, ServerHttpCookie> getCookies() {
if (State.COMITTED.equals(this.state.get())) {
return CollectionUtils.unmodifiableMultiValueMap(this.cookies);
}
return this.cookies;
}
@Override
public Mono<Void> setBody(Publisher<DataBuffer> publisher) {
return new WriteWithOperator<>(publisher, writePublisher ->

View File

@@ -18,9 +18,6 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.io.buffer.Buffer;
@@ -33,6 +30,7 @@ import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the Reactor Net {@link HttpChannel}.
@@ -76,15 +74,11 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
protected void initCookies(Map<String, List<HttpCookie>> cookies) {
protected void initCookies(MultiValueMap<String, HttpCookie> cookies) {
for (String name : this.channel.cookies().keySet()) {
List<HttpCookie> list = cookies.get(name);
if (list == null) {
list = new ArrayList<>();
cookies.put(name, list);
}
for (Cookie cookie : this.channel.cookies().get(name)) {
list.add(HttpCookie.clientCookie(name, cookie.value()));
HttpCookie httpCookie = new HttpCookie(name, cookie.value());
cookies.add(name, httpCookie);
}
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.http.server.reactive;
import java.time.Duration;
import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -26,8 +27,8 @@ import reactor.io.net.http.model.Cookie;
import reactor.io.net.http.model.Status;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.Assert;
/**
@@ -73,10 +74,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
@Override
protected void writeCookies() {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
Cookie reactorCookie = new ReactorCookie(httpCookie);
this.channel.addResponseCookie(name, reactorCookie);
for (String name : getCookies().keySet()) {
for (ServerHttpCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new ReactorCookie(httpCookie);
this.channel.addResponseCookie(name, cookie);
}
}
}
@@ -87,10 +88,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
*/
private final static class ReactorCookie extends Cookie {
private final HttpCookie httpCookie;
private final ServerHttpCookie httpCookie;
public ReactorCookie(HttpCookie httpCookie) {
public ReactorCookie(ServerHttpCookie httpCookie) {
this.httpCookie = httpCookie;
}
@@ -117,12 +118,14 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
@Override
public String domain() {
return this.httpCookie.getDomain();
Optional<String> domain = this.httpCookie.getDomain();
return (domain.isPresent() ? domain.get() : null);
}
@Override
public String path() {
return this.httpCookie.getPath();
Optional<String> path = this.httpCookie.getPath();
return (path.isPresent() ? path.get() : null);
}
@Override

View File

@@ -18,9 +18,6 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.cookie.Cookie;
@@ -35,6 +32,7 @@ import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the RxNetty {@link HttpServerRequest}.
@@ -79,15 +77,11 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
protected void initCookies(Map<String, List<HttpCookie>> map) {
protected void initCookies(MultiValueMap<String, HttpCookie> cookies) {
for (String name : this.request.getCookies().keySet()) {
List<HttpCookie> list = map.get(name);
if (list == null) {
list = new ArrayList<>();
map.put(name, list);
}
for (Cookie cookie : this.request.getCookies().get(name)) {
list.add(HttpCookie.clientCookie(name, cookie.value()));
HttpCookie httpCookie = new HttpCookie(name, cookie.value());
cookies.add(name, httpCookie);
}
}
}

View File

@@ -29,8 +29,8 @@ import rx.Observable;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.Assert;
/**
@@ -85,14 +85,18 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
@Override
protected void writeCookies() {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
for (String name : getCookies().keySet()) {
for (ServerHttpCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new DefaultCookie(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge(httpCookie.getMaxAge().getSeconds());
}
cookie.setDomain(httpCookie.getDomain());
cookie.setPath(httpCookie.getPath());
if (httpCookie.getDomain().isPresent()) {
cookie.setDomain(httpCookie.getDomain().get());
}
if (httpCookie.getPath().isPresent()) {
cookie.setPath(httpCookie.getPath().get());
}
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.response.addCookie(cookie);

View File

@@ -16,8 +16,13 @@
package org.springframework.http.server.reactive;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpRequest;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.util.MultiValueMap;
/**
* Represents a reactive server-side HTTP request
@@ -26,4 +31,9 @@ import org.springframework.http.ReactiveHttpInputMessage;
*/
public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
/**
* Return a read-only map of cookies sent by the client.
*/
MultiValueMap<String, HttpCookie> getCookies();
}

View File

@@ -16,10 +16,16 @@
package org.springframework.http.server.reactive;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.MultiValueMap;
/**
* Represents a reactive server-side HTTP response.
@@ -34,6 +40,11 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
*/
void setStatusCode(HttpStatus status);
/**
* Return a mutable map with cookies to be sent to the client.
*/
MultiValueMap<String, ServerHttpCookie> getCookies();
/**
* Indicate that request handling is complete, allowing for any cleanup or
* end-of-processing tasks to be performed such as applying header changes

View File

@@ -20,9 +20,7 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.ReadListener;
@@ -45,6 +43,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
@@ -127,17 +126,13 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
protected void initCookies(Map<String, List<HttpCookie>> map) {
protected void initCookies(MultiValueMap<String, HttpCookie> httpCookies) {
Cookie[] cookies = this.request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
List<HttpCookie> list = map.get(name);
if (list == null) {
list = new ArrayList<>();
map.put(name, list);
}
list.add(HttpCookie.clientCookie(name, cookie.getValue()));
HttpCookie httpCookie = new HttpCookie(name, cookie.getValue());
httpCookies.add(name, httpCookie);
}
}
}

View File

@@ -34,9 +34,9 @@ import org.reactivestreams.Subscription;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.Assert;
/**
@@ -98,17 +98,17 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse {
@Override
protected void writeCookies() {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
for (String name : getCookies().keySet()) {
for (ServerHttpCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new Cookie(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
if (httpCookie.getDomain() != null) {
cookie.setDomain(httpCookie.getDomain());
if (httpCookie.getDomain().isPresent()) {
cookie.setDomain(httpCookie.getDomain().get());
}
if (httpCookie.getPath() != null) {
cookie.setPath(httpCookie.getPath());
if (httpCookie.getPath().isPresent()) {
cookie.setPath(httpCookie.getPath().get());
}
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());

View File

@@ -18,9 +18,6 @@ package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
@@ -33,6 +30,7 @@ import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}.
@@ -79,15 +77,11 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest {
}
@Override
protected void initCookies(Map<String, List<HttpCookie>> map) {
protected void initCookies(MultiValueMap<String, HttpCookie> cookies) {
for (String name : this.exchange.getRequestCookies().keySet()) {
List<HttpCookie> list = map.get(name);
if (list == null) {
list = new ArrayList<>();
map.put(name, list);
}
Cookie cookie = this.exchange.getRequestCookies().get(name);
list.add(HttpCookie.clientCookie(name, cookie.getValue()));
HttpCookie httpCookie = new HttpCookie(name, cookie.getValue());
cookies.add(name, httpCookie);
}
}

View File

@@ -28,8 +28,8 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.Assert;
/**
@@ -80,14 +80,18 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse {
@Override
protected void writeCookies() {
for (String name : getHeaders().getCookies().keySet()) {
for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) {
for (String name : getCookies().keySet()) {
for (ServerHttpCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
cookie.setDomain(httpCookie.getDomain());
cookie.setPath(httpCookie.getPath());
if (httpCookie.getDomain().isPresent()) {
cookie.setDomain(httpCookie.getDomain().get());
}
if (httpCookie.getPath().isPresent()) {
cookie.setPath(httpCookie.getPath().get());
}
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.exchange.getResponseCookies().putIfAbsent(name, cookie);

View File

@@ -18,12 +18,14 @@ package org.springframework.web.server.session;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
@@ -58,7 +60,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver {
/**
* Set the value for the "Max-Age" attribute of the cookie that holds the
* session id. For the range of values see {@link HttpCookie#getMaxAge()}.
* session id. For the range of values see {@link ServerHttpCookie#getMaxAge()}.
* <p>By default set to -1.
* @param maxAge the maxAge duration value
*/
@@ -76,18 +78,17 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver {
@Override
public Optional<String> resolveSessionId(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
List<HttpCookie> cookies = headers.getCookies().get(getCookieName());
return (CollectionUtils.isEmpty(cookies) ?
Optional.empty() : Optional.of(cookies.get(0).getValue()));
MultiValueMap<String, HttpCookie> cookieMap = exchange.getRequest().getCookies();
HttpCookie cookie = cookieMap.getFirst(getCookieName());
return (cookie != null ? Optional.of(cookie.getValue()) : Optional.empty());
}
@Override
public void setSessionId(ServerWebExchange exchange, String id) {
Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0));
HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build();
HttpHeaders headers = exchange.getResponse().getHeaders();
headers.getCookies().put(getCookieName(), Collections.singletonList(cookie));
ServerHttpCookie cookie = ServerHttpCookie.with(getCookieName(), id).maxAge(maxAge).build();
MultiValueMap<String, ServerHttpCookie> cookieMap = exchange.getResponse().getCookies();
cookieMap.set(getCookieName(), cookie);
}
}

View File

@@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpCookie;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.ServerHttpCookie;
import org.springframework.web.client.RestTemplate;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -98,12 +99,12 @@ public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
this.requestCookies = request.getHeaders().getCookies();
this.requestCookies = request.getCookies();
this.requestCookies.size(); // Cause lazy loading
response.getHeaders().addCookie(HttpCookie.serverCookie("SID", "31d4d96e407aad42")
response.getCookies().add("SID", ServerHttpCookie.with("SID", "31d4d96e407aad42")
.path("/").secure().httpOnly().build());
response.getHeaders().addCookie(HttpCookie.serverCookie("lang", "en-US")
response.getCookies().add("lang", ServerHttpCookie.with("lang", "en-US")
.domain("example.com").path("/").build());
return response.setComplete();

View File

@@ -21,8 +21,11 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* @author Rossen Stoyanchev
@@ -35,6 +38,8 @@ public class MockServerHttpRequest implements ServerHttpRequest {
private HttpHeaders headers = new HttpHeaders();
private MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
private Flux<DataBuffer> body;
@@ -74,8 +79,9 @@ public class MockServerHttpRequest implements ServerHttpRequest {
return this.headers;
}
public void setHeaders(HttpHeaders headers) {
this.headers = headers;
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
return this.cookies;
}
@Override

View File

@@ -24,6 +24,9 @@ import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ServerHttpCookie;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* @author Rossen Stoyanchev
@@ -34,6 +37,8 @@ public class MockServerHttpResponse implements ServerHttpResponse {
private HttpHeaders headers = new HttpHeaders();
private MultiValueMap<String, ServerHttpCookie> cookies = new LinkedMultiValueMap<>();
private Publisher<DataBuffer> body;
@@ -51,6 +56,11 @@ public class MockServerHttpResponse implements ServerHttpResponse {
return this.headers;
}
@Override
public MultiValueMap<String, ServerHttpCookie> getCookies() {
return this.cookies;
}
public Publisher<DataBuffer> getBody() {
return this.body;
}

View File

@@ -27,8 +27,8 @@ import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.ServerHttpCookie;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
@@ -81,17 +81,17 @@ public class ServerHttpResponseTests {
@Test
public void beforeCommitWithSetBody() throws Exception {
HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build();
ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build();
TestServerHttpResponse response = new TestServerHttpResponse();
response.beforeCommit(() -> {
response.getHeaders().addCookie(cookie);
response.getCookies().add(cookie.getName(), cookie);
return Mono.empty();
});
response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get();
assertTrue(response.headersWritten);
assertTrue(response.cookiesWritten);
assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0));
assertSame(cookie, response.getCookies().getFirst("ID"));
assertEquals(3, response.content.size());
assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8));
@@ -108,7 +108,7 @@ public class ServerHttpResponseTests {
assertTrue("beforeCommit action errors should be ignored", response.headersWritten);
assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten);
assertNull(response.getHeaders().getCookies().get("ID"));
assertNull(response.getCookies().get("ID"));
assertEquals(3, response.content.size());
assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8));
@@ -118,10 +118,10 @@ public class ServerHttpResponseTests {
@Test
public void beforeCommitActionWithSetComplete() throws Exception {
HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build();
ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build();
TestServerHttpResponse response = new TestServerHttpResponse();
response.beforeCommit(() -> {
response.getHeaders().addCookie(cookie);
response.getCookies().add(cookie.getName(), cookie);
return Mono.empty();
});
response.setComplete().get();
@@ -129,7 +129,7 @@ public class ServerHttpResponseTests {
assertTrue(response.headersWritten);
assertTrue(response.cookiesWritten);
assertTrue(response.content.isEmpty());
assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0));
assertSame(cookie, response.getCookies().getFirst("ID"));
}