Skip to content

Commit 5938ca7

Browse files
committed
Fix request matcher management context support
Fix caching issues in `ApplicationContextRequestMatcher` and allow subclasses to ignore an application context entirely. Update existing matcher implementations so that they deal with the management context correctly. Prior to this commit, the `ApplicationContextRequestMatcher` would return a context cached from the first request. It also didn't provide any way to ignore a context. This meant that if the user was running the management server on a different port the matching results could be inconsistent depending on if the first request arrived on the regular context or the management context. It also meant that we could not distinguish between the regular context and the management context when matching. Closes gh-18012
1 parent 587e116 commit 5938ca7

File tree

10 files changed

+245
-54
lines changed

10 files changed

+245
-54
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3838
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
3939
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
40+
import org.springframework.boot.web.context.WebServerApplicationContext;
4041
import org.springframework.core.annotation.AnnotatedElementUtils;
4142
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4243
import org.springframework.security.web.util.matcher.OrRequestMatcher;
4344
import org.springframework.security.web.util.matcher.RequestMatcher;
4445
import org.springframework.util.Assert;
4546
import org.springframework.util.StringUtils;
4647
import org.springframework.web.context.WebApplicationContext;
47-
import org.springframework.web.context.support.WebApplicationContextUtils;
4848

4949
/**
5050
* Factory that can be used to create a {@link RequestMatcher} for actuator endpoint
@@ -127,24 +127,20 @@ private abstract static class AbstractRequestMatcher
127127
super(WebApplicationContext.class);
128128
}
129129

130+
@Override
131+
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
132+
ManagementPortType type = ManagementPortType.get(applicationContext.getEnvironment());
133+
return type == ManagementPortType.DIFFERENT
134+
&& WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
135+
}
136+
130137
@Override
131138
protected final void initialized(Supplier<WebApplicationContext> context) {
132139
this.delegate = createDelegate(context.get());
133140
}
134141

135142
@Override
136143
protected final boolean matches(HttpServletRequest request, Supplier<WebApplicationContext> context) {
137-
WebApplicationContext applicationContext = WebApplicationContextUtils
138-
.getRequiredWebApplicationContext(request.getServletContext());
139-
if (ManagementPortType.get(applicationContext.getEnvironment()) == ManagementPortType.DIFFERENT) {
140-
if (applicationContext.getParent() == null) {
141-
return false;
142-
}
143-
String managementContextId = applicationContext.getParent().getId() + ":management";
144-
if (!managementContextId.equals(applicationContext.getId())) {
145-
return false;
146-
}
147-
}
148144
return this.delegate.matches(request);
149145
}
150146

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/PathRequest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties;
2424
import org.springframework.boot.autoconfigure.security.StaticResourceLocation;
2525
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
26+
import org.springframework.boot.web.context.WebServerApplicationContext;
2627
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
2728
import org.springframework.security.web.util.matcher.RequestMatcher;
29+
import org.springframework.web.context.WebApplicationContext;
2830

2931
/**
3032
* Factory that can be used to create a {@link RequestMatcher} for commonly used paths.
@@ -69,6 +71,11 @@ private H2ConsoleRequestMatcher() {
6971
super(H2ConsoleProperties.class);
7072
}
7173

74+
@Override
75+
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
76+
return WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
77+
}
78+
7279
@Override
7380
protected void initialized(Supplier<H2ConsoleProperties> h2ConsoleProperties) {
7481
this.delegate = new AntPathRequestMatcher(h2ConsoleProperties.get().getPath() + "/**");

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/StaticResourceRequest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
import org.springframework.boot.autoconfigure.security.StaticResourceLocation;
3030
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
3131
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
32+
import org.springframework.boot.web.context.WebServerApplicationContext;
3233
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3334
import org.springframework.security.web.util.matcher.OrRequestMatcher;
3435
import org.springframework.security.web.util.matcher.RequestMatcher;
3536
import org.springframework.util.Assert;
37+
import org.springframework.web.context.WebApplicationContext;
3638

3739
/**
3840
* Used to create a {@link RequestMatcher} for static resources in commonly used
@@ -144,6 +146,11 @@ private Stream<String> getPatterns(DispatcherServletPath dispatcherServletPath)
144146
.map(dispatcherServletPath::getRelativePath);
145147
}
146148

149+
@Override
150+
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
151+
return WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
152+
}
153+
147154
@Override
148155
protected boolean matches(HttpServletRequest request, Supplier<DispatcherServletPath> context) {
149156
return this.delegate.matches(request);

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/PathRequestTests.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.springframework.mock.web.MockServletContext;
2828
import org.springframework.security.web.util.matcher.RequestMatcher;
2929
import org.springframework.web.context.WebApplicationContext;
30-
import org.springframework.web.context.support.StaticWebApplicationContext;
3130

3231
import static org.assertj.core.api.Assertions.assertThat;
3332

@@ -51,8 +50,20 @@ public void toH2ConsoleShouldMatchH2ConsolePath() {
5150
assertMatcher(matcher).doesNotMatch("/js/file.js");
5251
}
5352

53+
@Test
54+
public void toH2ConsoleWhenManagementContextShouldNeverMatch() {
55+
RequestMatcher matcher = PathRequest.toH2Console();
56+
assertMatcher(matcher, "management").doesNotMatch("/h2-console");
57+
assertMatcher(matcher, "management").doesNotMatch("/h2-console/subpath");
58+
assertMatcher(matcher, "management").doesNotMatch("/js/file.js");
59+
}
60+
5461
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
55-
StaticWebApplicationContext context = new StaticWebApplicationContext();
62+
return assertMatcher(matcher, null);
63+
}
64+
65+
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace) {
66+
TestWebApplicationContext context = new TestWebApplicationContext(serverNamespace);
5667
context.registerBean(ServerProperties.class);
5768
context.registerBean(H2ConsoleProperties.class);
5869
return assertThat(new RequestMatcherAssert(context, matcher));

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/StaticResourceRequestTests.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.springframework.mock.web.MockServletContext;
2828
import org.springframework.security.web.util.matcher.RequestMatcher;
2929
import org.springframework.web.context.WebApplicationContext;
30-
import org.springframework.web.context.support.StaticWebApplicationContext;
3130

3231
import static org.assertj.core.api.Assertions.assertThat;
3332
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -53,6 +52,16 @@ public void atCommonLocationsShouldMatchCommonLocations() {
5352
assertMatcher(matcher).doesNotMatch("/bar");
5453
}
5554

55+
@Test
56+
public void atCommonLocationsWhenManagementContextShouldNeverMatch() {
57+
RequestMatcher matcher = this.resourceRequest.atCommonLocations();
58+
assertMatcher(matcher, "management").doesNotMatch("/css/file.css");
59+
assertMatcher(matcher, "management").doesNotMatch("/js/file.js");
60+
assertMatcher(matcher, "management").doesNotMatch("/images/file.css");
61+
assertMatcher(matcher, "management").doesNotMatch("/webjars/file.css");
62+
assertMatcher(matcher, "management").doesNotMatch("/foo/favicon.ico");
63+
}
64+
5665
@Test
5766
public void atCommonLocationsWithExcludeShouldNotMatchExcluded() {
5867
RequestMatcher matcher = this.resourceRequest.atCommonLocations().excluding(StaticResourceLocation.CSS);
@@ -70,8 +79,8 @@ public void atLocationShouldMatchLocation() {
7079
@Test
7180
public void atLocationWhenHasServletPathShouldMatchLocation() {
7281
RequestMatcher matcher = this.resourceRequest.at(StaticResourceLocation.CSS);
73-
assertMatcher(matcher, "/foo").matches("/foo", "/css/file.css");
74-
assertMatcher(matcher, "/foo").doesNotMatch("/foo", "/js/file.js");
82+
assertMatcher(matcher, null, "/foo").matches("/foo", "/css/file.css");
83+
assertMatcher(matcher, null, "/foo").doesNotMatch("/foo", "/js/file.js");
7584
}
7685

7786
@Test
@@ -87,15 +96,16 @@ public void excludeFromSetWhenSetIsNullShouldThrowException() {
8796
}
8897

8998
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
90-
DispatcherServletPath dispatcherServletPath = () -> "";
91-
StaticWebApplicationContext context = new StaticWebApplicationContext();
92-
context.registerBean(DispatcherServletPath.class, () -> dispatcherServletPath);
93-
return assertThat(new RequestMatcherAssert(context, matcher));
99+
return assertMatcher(matcher, null, "");
100+
}
101+
102+
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace) {
103+
return assertMatcher(matcher, serverNamespace, "");
94104
}
95105

96-
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String path) {
106+
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace, String path) {
97107
DispatcherServletPath dispatcherServletPath = () -> path;
98-
StaticWebApplicationContext context = new StaticWebApplicationContext();
108+
TestWebApplicationContext context = new TestWebApplicationContext(serverNamespace);
99109
context.registerBean(DispatcherServletPath.class, () -> dispatcherServletPath);
100110
return assertThat(new RequestMatcherAssert(context, matcher));
101111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.security.servlet;
18+
19+
import org.springframework.boot.web.context.WebServerApplicationContext;
20+
import org.springframework.boot.web.server.WebServer;
21+
import org.springframework.web.context.support.StaticWebApplicationContext;
22+
23+
/**
24+
* Test {@link StaticWebApplicationContext} that also implements
25+
* {@link WebServerApplicationContext}.
26+
*
27+
* @author Phillip Webb
28+
*/
29+
class TestWebApplicationContext extends StaticWebApplicationContext implements WebServerApplicationContext {
30+
31+
private final String serverNamespace;
32+
33+
TestWebApplicationContext(String serverNamespace) {
34+
this.serverNamespace = serverNamespace;
35+
}
36+
37+
@Override
38+
public WebServer getWebServer() {
39+
return null;
40+
}
41+
42+
@Override
43+
public String getServerNamespace() {
44+
return this.serverNamespace;
45+
}
46+
47+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.security.servlet;
1818

19+
import java.util.concurrent.atomic.AtomicBoolean;
1920
import java.util.function.Supplier;
2021

2122
import javax.servlet.http.HttpServletRequest;
@@ -43,9 +44,7 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc
4344

4445
private final Class<? extends C> contextClass;
4546

46-
private volatile Supplier<C> context;
47-
48-
private final Object contextLock = new Object();
47+
private final AtomicBoolean initialized = new AtomicBoolean(false);
4948

5049
public ApplicationContextRequestMatcher(Class<? extends C> contextClass) {
5150
Assert.notNull(contextClass, "Context class must not be null");
@@ -54,45 +53,56 @@ public ApplicationContextRequestMatcher(Class<? extends C> contextClass) {
5453

5554
@Override
5655
public final boolean matches(HttpServletRequest request) {
57-
return matches(request, getContext(request));
56+
WebApplicationContext webApplicationContext = WebApplicationContextUtils
57+
.getRequiredWebApplicationContext(request.getServletContext());
58+
if (ignoreApplicationContext(webApplicationContext)) {
59+
return false;
60+
}
61+
Supplier<C> context = () -> getContext(webApplicationContext);
62+
if (this.initialized.compareAndSet(false, true)) {
63+
initialized(context);
64+
}
65+
return matches(request, context);
66+
}
67+
68+
@SuppressWarnings("unchecked")
69+
private C getContext(WebApplicationContext webApplicationContext) {
70+
if (this.contextClass.isInstance(webApplicationContext)) {
71+
return (C) webApplicationContext;
72+
}
73+
return webApplicationContext.getBean(this.contextClass);
5874
}
5975

6076
/**
61-
* Decides whether the rule implemented by the strategy matches the supplied request.
62-
* @param request the source request
63-
* @param context a supplier for the initialized context (may throw an exception)
64-
* @return if the request matches
77+
* Returns if the {@link WebApplicationContext} should be ignored and not used for
78+
* matching. If this method returns {@code true} then the context will not be used and
79+
* the {@link #matches(HttpServletRequest) matches} method will return {@code false}.
80+
* @param webApplicationContext the candidate web application context
81+
* @return if the application context should be ignored
82+
* @since 2.1.8
6583
*/
66-
protected abstract boolean matches(HttpServletRequest request, Supplier<C> context);
67-
68-
private Supplier<C> getContext(HttpServletRequest request) {
69-
if (this.context == null) {
70-
synchronized (this.contextLock) {
71-
if (this.context == null) {
72-
Supplier<C> createdContext = createContext(request);
73-
initialized(createdContext);
74-
this.context = createdContext;
75-
}
76-
}
77-
}
78-
return this.context;
84+
protected boolean ignoreApplicationContext(WebApplicationContext webApplicationContext) {
85+
return false;
7986
}
8087

8188
/**
82-
* Called once the context has been initialized.
89+
* Method that can be implemented by subclasses that wish to initialize items the
90+
* first time that the matcher is called. This method will be called only once and
91+
* only if {@link #ignoreApplicationContext(WebApplicationContext)} returns
92+
* {@code true}. Note that the supplied context will be based on the
93+
* <strong>first</strong> request sent to the matcher.
8394
* @param context a supplier for the initialized context (may throw an exception)
95+
* @see #ignoreApplicationContext(WebApplicationContext)
8496
*/
8597
protected void initialized(Supplier<C> context) {
8698
}
8799

88-
@SuppressWarnings("unchecked")
89-
private Supplier<C> createContext(HttpServletRequest request) {
90-
WebApplicationContext context = WebApplicationContextUtils
91-
.getRequiredWebApplicationContext(request.getServletContext());
92-
if (this.contextClass.isInstance(context)) {
93-
return () -> (C) context;
94-
}
95-
return () -> context.getBean(this.contextClass);
96-
}
100+
/**
101+
* Decides whether the rule implemented by the strategy matches the supplied request.
102+
* @param request the source request
103+
* @param context a supplier for the initialized context (may throw an exception)
104+
* @return if the request matches
105+
*/
106+
protected abstract boolean matches(HttpServletRequest request, Supplier<C> context);
97107

98108
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/WebServerApplicationContext.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.springframework.boot.web.server.WebServer;
2020
import org.springframework.context.ApplicationContext;
21+
import org.springframework.util.ObjectUtils;
2122

2223
/**
2324
* Interface to be implemented by {@link ApplicationContext application contexts} that
@@ -44,4 +45,17 @@ public interface WebServerApplicationContext extends ApplicationContext {
4445
*/
4546
String getServerNamespace();
4647

48+
/**
49+
* Returns {@code true} if the specified context is a
50+
* {@link WebServerApplicationContext} with a matching server namespace.
51+
* @param context the context to check
52+
* @param serverNamespace the server namespace to match against
53+
* @return {@code true} if the server namespace of the context matches
54+
* @since 2.1.8
55+
*/
56+
static boolean hasServerNamespace(ApplicationContext context, String serverNamespace) {
57+
return (context instanceof WebServerApplicationContext) && ObjectUtils
58+
.nullSafeEquals(((WebServerApplicationContext) context).getServerNamespace(), serverNamespace);
59+
}
60+
4761
}

0 commit comments

Comments
 (0)