Serve static content (css, js) for reactive default UIs from DefaultResourcesWebFilter
This commit is contained in:
parent
11616a1d78
commit
45d53973ab
|
@ -194,6 +194,7 @@ import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
|||
import org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter;
|
||||
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
|
||||
import org.springframework.security.web.server.transport.HttpsRedirectWebFilter;
|
||||
import org.springframework.security.web.server.ui.DefaultResourcesWebFilter;
|
||||
import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter;
|
||||
import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter;
|
||||
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
|
||||
|
@ -2974,6 +2975,7 @@ public class ServerHttpSecurity {
|
|||
}
|
||||
if (loginPage != null) {
|
||||
http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
|
||||
http.addFilterBefore(DefaultResourcesWebFilter.css(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
|
||||
if (http.logout != null) {
|
||||
http.addFilterAt(new LogoutPageGeneratingWebFilter(),
|
||||
SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.util.Optional;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -738,6 +739,40 @@ public class ServerHttpSecurityTests {
|
|||
.isSameAs(authorizationRedirectStrategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resourcesWhenLoginPageConfiguredThenServesCss() {
|
||||
this.http.formLogin(withDefaults());
|
||||
this.http.authenticationManager(this.authenticationManager);
|
||||
WebTestClient client = WebTestClientBuilder
|
||||
.bindToControllerAndWebFilters(NotFoundController.class, this.http.build())
|
||||
.build();
|
||||
|
||||
client.get()
|
||||
.uri("/default-ui.css")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.value(Matchers.containsString("body {"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resourcesWhenLoginPageNotConfiguredThenDoesNotServeCss() {
|
||||
this.http.httpBasic(withDefaults());
|
||||
this.http.authenticationManager(this.authenticationManager);
|
||||
WebTestClient client = WebTestClientBuilder
|
||||
.bindToControllerAndWebFilters(NotFoundController.class, this.http.build())
|
||||
.build();
|
||||
|
||||
client.get()
|
||||
.uri("/default-ui.css")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNotFound()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo(null);
|
||||
}
|
||||
|
||||
private boolean isX509Filter(WebFilter filter) {
|
||||
try {
|
||||
Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");
|
||||
|
@ -775,6 +810,13 @@ public class ServerHttpSecurityTests {
|
|||
|
||||
}
|
||||
|
||||
@RestController
|
||||
private static class NotFoundController {
|
||||
|
||||
// Empty controller, makes WebTestClient return HTTP 404
|
||||
|
||||
}
|
||||
|
||||
private static class TestWebFilter implements WebFilter {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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.security.web.server.ui;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
/**
|
||||
* Serve common static assets used in default UIs, such as CSS or Javascript files. For
|
||||
* internal use only.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 6.4
|
||||
*/
|
||||
public final class DefaultResourcesWebFilter implements WebFilter {
|
||||
|
||||
private final ServerWebExchangeMatcher matcher;
|
||||
|
||||
private final ClassPathResource resource;
|
||||
|
||||
private final MediaType mediaType;
|
||||
|
||||
private DefaultResourcesWebFilter(ServerWebExchangeMatcher matcher, ClassPathResource resource,
|
||||
MediaType mediaType) {
|
||||
Assert.isTrue(resource.exists(), "classpath resource must exist");
|
||||
this.matcher = matcher;
|
||||
this.resource = resource;
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.matcher.matches(exchange)
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap((matchResult) -> sendContent(exchange));
|
||||
}
|
||||
|
||||
private Mono<Void> sendContent(ServerWebExchange exchange) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.OK);
|
||||
ResourceHttpMessageWriter writer = new ResourceHttpMessageWriter();
|
||||
return writer.write(Mono.just(this.resource), ResolvableType.forClass(Resource.class),
|
||||
ResolvableType.forClass(Resource.class), this.mediaType, exchange.getRequest(), exchange.getResponse(),
|
||||
Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "%s{matcher=%s, resource='%s'}".formatted(getClass().getSimpleName(), this.matcher,
|
||||
this.resource.getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of {@link DefaultResourcesWebFilter} serving Spring Security's
|
||||
* default CSS stylesheet.
|
||||
* <p>
|
||||
* The created {@link DefaultResourcesFilter} matches requests
|
||||
* {@code HTTP GET /default-ui.css}, and returns the default stylesheet at
|
||||
* {@code org/springframework/security/default-ui.css} with content-type
|
||||
* {@code text/css;charset=UTF-8}.
|
||||
* @return -
|
||||
*/
|
||||
public static DefaultResourcesWebFilter css() {
|
||||
return new DefaultResourcesWebFilter(
|
||||
new PathPatternParserServerWebExchangeMatcher("/default-ui.css", HttpMethod.GET),
|
||||
new ClassPathResource("org/springframework/security/default-ui.css"),
|
||||
new MediaType("text", "css", StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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.security.web.server.ui;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.handler.DefaultWebFilterChain;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 6.4
|
||||
*/
|
||||
class DefaultResourcesWebFilterTests {
|
||||
|
||||
private final WebHandler notFoundHandler = (exchange) -> {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return Mono.empty();
|
||||
};
|
||||
|
||||
private final DefaultResourcesWebFilter filter = DefaultResourcesWebFilter.css();
|
||||
|
||||
@Test
|
||||
void filterWhenPathMatchesThenRenders() {
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/default-ui.css"));
|
||||
WebFilterChain filterChain = new DefaultWebFilterChain(this.notFoundHandler, List.of(this.filter));
|
||||
|
||||
filterChain.filter(exchange).block();
|
||||
|
||||
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(exchange.getResponse().getHeaders().getContentType())
|
||||
.isEqualTo(new MediaType("text", "css", StandardCharsets.UTF_8));
|
||||
assertThat(exchange.getResponse().getBodyAsString().block()).contains("body {");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenPathDoesNotMatchThenCallsThrough() {
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/does-not-match"));
|
||||
WebFilterChain filterChain = new DefaultWebFilterChain(this.notFoundHandler, List.of(this.filter));
|
||||
|
||||
filterChain.filter(exchange).block();
|
||||
|
||||
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringPrintsPathAndResource() {
|
||||
assertThat(this.filter.toString()).isEqualTo(
|
||||
"DefaultResourcesWebFilter{matcher=PathMatcherServerWebExchangeMatcher{pattern='/default-ui.css', method=GET}, resource='org/springframework/security/default-ui.css'}");
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue