11 changed files with 400 additions and 69 deletions
@ -0,0 +1,64 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2012-2017 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.boot.autoconfigure.security; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
/** |
||||||
|
* Common locations for static resources. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public enum StaticResourceLocation { |
||||||
|
|
||||||
|
/** |
||||||
|
* Resources under {@code "/css"}. |
||||||
|
*/ |
||||||
|
CSS("/css/**"), |
||||||
|
|
||||||
|
/** |
||||||
|
* Resources under {@code "/js"}. |
||||||
|
*/ |
||||||
|
JAVA_SCRIPT("/js/**"), |
||||||
|
|
||||||
|
/** |
||||||
|
* Resources under {@code "/images"}. |
||||||
|
*/ |
||||||
|
IMAGES("/images/**"), |
||||||
|
|
||||||
|
/** |
||||||
|
* Resources under {@code "/webjars"}. |
||||||
|
*/ |
||||||
|
WEB_JARS("/webjars/**"), |
||||||
|
|
||||||
|
/** |
||||||
|
* The {@code "favicon.ico"} resource. |
||||||
|
*/ |
||||||
|
FAVICON("/**/favicon.ico"); |
||||||
|
|
||||||
|
private String[] patterns; |
||||||
|
|
||||||
|
StaticResourceLocation(String... patterns) { |
||||||
|
this.patterns = patterns; |
||||||
|
} |
||||||
|
|
||||||
|
public Stream<String> getPatterns() { |
||||||
|
return Arrays.stream(this.patterns); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2012-2018 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.boot.autoconfigure.security.reactive; |
||||||
|
|
||||||
|
import java.util.EnumSet; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.security.StaticResourceLocation; |
||||||
|
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; |
||||||
|
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; |
||||||
|
|
||||||
|
/** |
||||||
|
* Factory that can be used to create a {@link ServerWebExchangeMatcher} for static resources in |
||||||
|
* commonly used locations. |
||||||
|
* |
||||||
|
* @author Madhura Bhave |
||||||
|
* @since 2.0.0 |
||||||
|
*/ |
||||||
|
public final class StaticResourceRequest { |
||||||
|
|
||||||
|
private StaticResourceRequest() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a matcher that includes all commonly used {@link StaticResourceLocation Locations}. The |
||||||
|
* {@link StaticResourceServerWebExchange#excluding(StaticResourceLocation, StaticResourceLocation...) excluding} |
||||||
|
* method can be used to remove specific locations if required. For example: |
||||||
|
* <pre class="code"> |
||||||
|
* StaticResourceRequest.toCommonLocations().excluding(StaticResourceLocation.CSS) |
||||||
|
* </pre> |
||||||
|
* @return the configured {@link ServerWebExchangeMatcher} |
||||||
|
*/ |
||||||
|
public static StaticResourceServerWebExchange toCommonLocations() { |
||||||
|
return to(EnumSet.allOf(StaticResourceLocation.class)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a matcher that includes the specified {@link StaticResourceLocation Locations}. For |
||||||
|
* example: <pre class="code"> |
||||||
|
* to(StaticResourceLocation.CSS, StaticResourceLocation.JAVA_SCRIPT) |
||||||
|
* </pre> |
||||||
|
* @param first the first location to include |
||||||
|
* @param rest additional locations to include |
||||||
|
* @return the configured {@link ServerWebExchangeMatcher} |
||||||
|
*/ |
||||||
|
public static StaticResourceServerWebExchange to(StaticResourceLocation first, StaticResourceLocation... rest) { |
||||||
|
return to(EnumSet.of(first, rest)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a matcher that includes the specified {@link StaticResourceLocation Locations}. For |
||||||
|
* example: <pre class="code"> |
||||||
|
* to(locations) |
||||||
|
* </pre> |
||||||
|
* @param locations the locations to include |
||||||
|
* @return the configured {@link ServerWebExchangeMatcher} |
||||||
|
*/ |
||||||
|
public static StaticResourceServerWebExchange to(Set<StaticResourceLocation> locations) { |
||||||
|
Assert.notNull(locations, "Locations must not be null"); |
||||||
|
return new StaticResourceServerWebExchange(new LinkedHashSet<>(locations)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The server web exchange matcher used to match against resource {@link StaticResourceLocation Locations}. |
||||||
|
*/ |
||||||
|
public final static class StaticResourceServerWebExchange |
||||||
|
implements ServerWebExchangeMatcher { |
||||||
|
|
||||||
|
private final Set<StaticResourceLocation> locations; |
||||||
|
|
||||||
|
private StaticResourceServerWebExchange(Set<StaticResourceLocation> locations) { |
||||||
|
this.locations = locations; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a new {@link StaticResourceServerWebExchange} based on this one but |
||||||
|
* excluding the specified locations. |
||||||
|
* @param first the first location to exclude |
||||||
|
* @param rest additional locations to exclude |
||||||
|
* @return a new {@link StaticResourceServerWebExchange} |
||||||
|
*/ |
||||||
|
public StaticResourceServerWebExchange excluding(StaticResourceLocation first, StaticResourceLocation... rest) { |
||||||
|
return excluding(EnumSet.of(first, rest)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a new {@link StaticResourceServerWebExchange} based on this one but |
||||||
|
* excluding the specified locations. |
||||||
|
* @param locations the locations to exclude |
||||||
|
* @return a new {@link StaticResourceServerWebExchange} |
||||||
|
*/ |
||||||
|
public StaticResourceServerWebExchange excluding(Set<StaticResourceLocation> locations) { |
||||||
|
Assert.notNull(locations, "Locations must not be null"); |
||||||
|
Set<StaticResourceLocation> subset = new LinkedHashSet<>(this.locations); |
||||||
|
subset.removeAll(locations); |
||||||
|
return new StaticResourceServerWebExchange(subset); |
||||||
|
} |
||||||
|
|
||||||
|
private List<ServerWebExchangeMatcher> getDelegateMatchers() { |
||||||
|
return getPatterns().map(PathPatternParserServerWebExchangeMatcher::new) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
} |
||||||
|
|
||||||
|
private Stream<String> getPatterns() { |
||||||
|
return this.locations.stream().flatMap(StaticResourceLocation::getPatterns); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<MatchResult> matches(ServerWebExchange exchange) { |
||||||
|
OrServerWebExchangeMatcher matcher = new OrServerWebExchangeMatcher(getDelegateMatchers()); |
||||||
|
return matcher.matches(exchange); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,152 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2012-2018 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.boot.autoconfigure.security.reactive; |
||||||
|
|
||||||
|
import org.assertj.core.api.AssertDelegateTarget; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.ExpectedException; |
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.security.StaticResourceLocation; |
||||||
|
import org.springframework.boot.autoconfigure.web.ServerProperties; |
||||||
|
import org.springframework.context.support.StaticApplicationContext; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; |
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpResponse; |
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; |
||||||
|
import org.springframework.web.context.support.StaticWebApplicationContext; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import org.springframework.web.server.WebHandler; |
||||||
|
import org.springframework.web.server.adapter.HttpWebHandlerAdapter; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link StaticResourceRequest}. |
||||||
|
* |
||||||
|
* @author Madhura Bhave |
||||||
|
*/ |
||||||
|
public class StaticResourceRequestTests { |
||||||
|
|
||||||
|
@Rule |
||||||
|
public ExpectedException thrown = ExpectedException.none(); |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toCommonLocationsShouldMatchCommonLocations() { |
||||||
|
ServerWebExchangeMatcher matcher = StaticResourceRequest.toCommonLocations(); |
||||||
|
assertMatcher(matcher).matches("/css/file.css"); |
||||||
|
assertMatcher(matcher).matches("/js/file.js"); |
||||||
|
assertMatcher(matcher).matches("/images/file.css"); |
||||||
|
assertMatcher(matcher).matches("/webjars/file.css"); |
||||||
|
assertMatcher(matcher).matches("/foo/favicon.ico"); |
||||||
|
assertMatcher(matcher).doesNotMatch("/bar"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toCommonLocationsWithExcludeShouldNotMatchExcluded() { |
||||||
|
ServerWebExchangeMatcher matcher = StaticResourceRequest.toCommonLocations() |
||||||
|
.excluding(StaticResourceLocation.CSS); |
||||||
|
assertMatcher(matcher).doesNotMatch("/css/file.css"); |
||||||
|
assertMatcher(matcher).matches("/js/file.js"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toLocationShouldMatchLocation() { |
||||||
|
ServerWebExchangeMatcher matcher = StaticResourceRequest.to(StaticResourceLocation.CSS); |
||||||
|
assertMatcher(matcher).matches("/css/file.css"); |
||||||
|
assertMatcher(matcher).doesNotMatch("/js/file.js"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toLocationsFromSetWhenSetIsNullShouldThrowException() { |
||||||
|
this.thrown.expect(IllegalArgumentException.class); |
||||||
|
this.thrown.expectMessage("Locations must not be null"); |
||||||
|
StaticResourceRequest.to(null); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void excludeFromSetWhenSetIsNullShouldThrowException() { |
||||||
|
this.thrown.expect(IllegalArgumentException.class); |
||||||
|
this.thrown.expectMessage("Locations must not be null"); |
||||||
|
StaticResourceRequest.toCommonLocations().excluding(null); |
||||||
|
} |
||||||
|
|
||||||
|
private StaticResourceRequestTests.RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) { |
||||||
|
StaticWebApplicationContext context = new StaticWebApplicationContext(); |
||||||
|
context.registerBean(ServerProperties.class); |
||||||
|
return assertThat(new StaticResourceRequestTests.RequestMatcherAssert(context, matcher)); |
||||||
|
} |
||||||
|
|
||||||
|
private static class RequestMatcherAssert implements AssertDelegateTarget { |
||||||
|
|
||||||
|
private final StaticApplicationContext context; |
||||||
|
|
||||||
|
private final ServerWebExchangeMatcher matcher; |
||||||
|
|
||||||
|
RequestMatcherAssert(StaticApplicationContext context, ServerWebExchangeMatcher matcher) { |
||||||
|
this.context = context; |
||||||
|
this.matcher = matcher; |
||||||
|
} |
||||||
|
|
||||||
|
void matches(String path) { |
||||||
|
ServerWebExchange exchange = webHandler().createExchange(MockServerHttpRequest.get(path).build(), new MockServerHttpResponse()); |
||||||
|
matches(exchange); |
||||||
|
} |
||||||
|
|
||||||
|
private void matches(ServerWebExchange exchange) { |
||||||
|
assertThat(this.matcher.matches(exchange).block().isMatch()) |
||||||
|
.as("Matches " + getRequestPath(exchange)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
void doesNotMatch(String path) { |
||||||
|
ServerWebExchange exchange = webHandler().createExchange(MockServerHttpRequest.get(path).build(), new MockServerHttpResponse()); |
||||||
|
doesNotMatch(exchange); |
||||||
|
} |
||||||
|
|
||||||
|
private void doesNotMatch(ServerWebExchange exchange) { |
||||||
|
assertThat(this.matcher.matches(exchange).block().isMatch()) |
||||||
|
.as("Does not match " + getRequestPath(exchange)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
private TestHttpWebHandlerAdapter webHandler() { |
||||||
|
TestHttpWebHandlerAdapter adapter = new TestHttpWebHandlerAdapter(mock(WebHandler.class)); |
||||||
|
adapter.setApplicationContext(this.context); |
||||||
|
return adapter; |
||||||
|
} |
||||||
|
|
||||||
|
private String getRequestPath(ServerWebExchange exchange) { |
||||||
|
return exchange.getRequest().getPath().toString(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private static class TestHttpWebHandlerAdapter extends HttpWebHandlerAdapter { |
||||||
|
|
||||||
|
TestHttpWebHandlerAdapter(WebHandler delegate) { |
||||||
|
super(delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) { |
||||||
|
return super.createExchange(request, response); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue