diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index a5163b2292..ac87173920 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -19,6 +19,7 @@ package org.springframework.http.server.reactive; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -66,6 +67,8 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private final MultiValueMap cookies; + private Function urlEncoder = url -> url; + private final AtomicReference state = new AtomicReference<>(State.NEW); private final List>> commitActions = new ArrayList<>(4); @@ -117,6 +120,16 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { CollectionUtils.unmodifiableMultiValueMap(this.cookies) : this.cookies); } + @Override + public String encodeUrl(String url) { + return (this.urlEncoder != null ? this.urlEncoder.apply(url) : url); + } + + @Override + public void registerUrlEncoder(Function encoder) { + this.urlEncoder = (this.urlEncoder != null ? this.urlEncoder.andThen(encoder) : encoder); + } + @Override public void beforeCommit(Supplier> action) { if (action != null) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 8f550b623e..a556ea4a8a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive; +import java.util.function.Function; + import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; @@ -50,6 +52,24 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { */ MultiValueMap getCookies(); + /** + * A mechanism for URL rewriting that applications and libraries such as + * HTML template libraries to use consistently for all URLs emitted by + * the application. Doing so enables the registration of URL encoders via + * {@link #registerUrlEncoder} that can insert an id for authentication, + * a nonce for CSRF protection, a version for a static resource, etc. + * @param url the URL to encode + * @return the encoded URL or the same + */ + String encodeUrl(String url); + + /** + * Register a URL rewriting function for use with {@link #encodeUrl}. + * The function must return an encoded URL or the same URL. + * @param encoder a URL encoding function to use + */ + void registerUrlEncoder(Function encoder); + /** * Indicate that request handling is complete, allowing for any cleanup or * end-of-processing tasks to be performed such as applying header changes diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index fd875411ca..8bb3b2238b 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -42,6 +42,27 @@ import static org.junit.Assert.assertSame; */ public class ServerHttpResponseTests { + @Test + public void encodeUrlDefault() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + assertEquals("/foo", response.encodeUrl("/foo")); + } + + @Test + public void encodeUrlWithEncoder() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + response.registerUrlEncoder(s -> s + "?nonce=123"); + assertEquals("/foo?nonce=123", response.encodeUrl("/foo")); + } + + @Test + public void encodeUrlWithMultipleEncoders() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + response.registerUrlEncoder(s -> s + ";p=abc"); + response.registerUrlEncoder(s -> s + "?q=123"); + assertEquals("/foo;p=abc?q=123", response.encodeUrl("/foo")); + } + @Test public void writeWith() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); diff --git a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpResponse.java b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpResponse.java index fd7ae11364..e7f0984c69 100644 --- a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpResponse.java +++ b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpResponse.java @@ -18,6 +18,7 @@ package org.springframework.mock.http.server.reactive.test; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.function.Function; import java.util.function.Supplier; import org.reactivestreams.Publisher; @@ -49,6 +50,8 @@ public class MockServerHttpResponse implements ServerHttpResponse { private final MultiValueMap cookies = new LinkedMultiValueMap<>(); + private Function urlEncoder = url -> url; + private Flux body; private Flux> bodyWithFlushes; @@ -77,6 +80,16 @@ public class MockServerHttpResponse implements ServerHttpResponse { return this.cookies; } + @Override + public String encodeUrl(String url) { + return (this.urlEncoder != null ? this.urlEncoder.apply(url) : url); + } + + @Override + public void registerUrlEncoder(Function encoder) { + this.urlEncoder = (this.urlEncoder != null ? this.urlEncoder.andThen(encoder) : encoder); + } + public Publisher getBody() { return this.body; }