Add forServletPattern

Closes gh-13562
This commit is contained in:
Josh Cummings 2023-10-11 14:18:11 -06:00
parent 7e9d707c7d
commit 762319b6be
17 changed files with 2196 additions and 82 deletions

View File

@ -0,0 +1,52 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.web.util.matcher.RequestMatcher;
abstract class AbstractRequestMatcherBuilderRegistry<C> extends AbstractRequestMatcherRegistry<C> {
private final RequestMatcherBuilder builder;
AbstractRequestMatcherBuilderRegistry(ApplicationContext context) {
this(context, RequestMatcherBuilders.createDefault(context));
}
AbstractRequestMatcherBuilderRegistry(ApplicationContext context, RequestMatcherBuilder builder) {
setApplicationContext(context);
this.builder = builder;
}
@Override
public final C requestMatchers(String... patterns) {
return requestMatchers(null, patterns);
}
@Override
public final C requestMatchers(HttpMethod method, String... patterns) {
return requestMatchers(this.builder.matchers(method, patterns).toArray(RequestMatcher[]::new));
}
@Override
public final C requestMatchers(HttpMethod method) {
return requestMatchers(method, "/**");
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
final class AntPathRequestMatcherBuilder implements RequestMatcherBuilder {
private final String servletPath;
private AntPathRequestMatcherBuilder(String servletPath) {
this.servletPath = servletPath;
}
static AntPathRequestMatcherBuilder absolute() {
return new AntPathRequestMatcherBuilder(null);
}
static AntPathRequestMatcherBuilder relativeTo(String path) {
return new AntPathRequestMatcherBuilder(path);
}
@Override
public AntPathRequestMatcher matcher(String pattern) {
return matcher((String) null, pattern);
}
@Override
public AntPathRequestMatcher matcher(HttpMethod method, String pattern) {
return matcher((method != null) ? method.name() : null, pattern);
}
private AntPathRequestMatcher matcher(String method, String pattern) {
return new AntPathRequestMatcher(prependServletPath(pattern), method);
}
private String prependServletPath(String pattern) {
if (this.servletPath == null) {
return pattern;
}
return this.servletPath + pattern;
}
}

View File

@ -16,10 +16,14 @@
package org.springframework.security.config.annotation.web.configurers; package org.springframework.security.config.annotation.web.configurers;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -32,17 +36,22 @@ import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.ObservationAuthorizationManager;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.function.SingletonSupplier; import org.springframework.util.function.SingletonSupplier;
import org.springframework.web.servlet.DispatcherServlet;
/** /**
* Adds a URL based authorization using {@link AuthorizationManager}. * Adds a URL based authorization using {@link AuthorizationManager}.
@ -137,41 +146,62 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @author Evgeniy Cheban * @author Evgeniy Cheban
*/ */
public final class AuthorizationManagerRequestMatcherRegistry public final class AuthorizationManagerRequestMatcherRegistry
extends AbstractRequestMatcherRegistry<AuthorizedUrl> { extends AbstractRequestMatcherBuilderRegistry<AuthorizedUrl<AuthorizationManagerRequestMatcherRegistry>> {
private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager
.builder(); .builder();
private List<RequestMatcher> unmappedMatchers; List<RequestMatcher> unmappedMatchers;
private int mappingCount; private int mappingCount;
private boolean shouldFilterAllDispatcherTypes = true; private boolean shouldFilterAllDispatcherTypes = true;
private AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) { private final Map<String, AuthorizationManagerServletRequestMatcherRegistry> servletPattern = new LinkedHashMap<>();
setApplicationContext(context);
AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) {
super(context);
} }
private void addMapping(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) { private void addMapping(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.isTrue(this.servletPattern.isEmpty(),
"Since you have used forServletPattern, all request matchers must be configured using forServletPattern; alternatively, you can use requestMatchers(RequestMatcher) for all requests.");
this.unmappedMatchers = null; this.unmappedMatchers = null;
this.managerBuilder.add(matcher, manager); this.managerBuilder.add(matcher, manager);
this.mappingCount++; this.mappingCount++;
} }
private void addFirst(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) { private void addFirst(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.isTrue(this.servletPattern.isEmpty(),
"Since you have used forServletPattern, all request matchers must be configured using forServletPattern; alternatively, you can use requestMatchers(RequestMatcher) for all requests.");
this.unmappedMatchers = null; this.unmappedMatchers = null;
this.managerBuilder.mappings((m) -> m.add(0, new RequestMatcherEntry<>(matcher, manager))); this.managerBuilder.mappings((m) -> m.add(0, new RequestMatcherEntry<>(matcher, manager)));
this.mappingCount++; this.mappingCount++;
} }
private AuthorizationManager<HttpServletRequest> createAuthorizationManager() { private AuthorizationManager<HttpServletRequest> servletAuthorizationManager() {
for (Map.Entry<String, AuthorizationManagerServletRequestMatcherRegistry> entry : this.servletPattern
.entrySet()) {
AuthorizationManagerServletRequestMatcherRegistry registry = entry.getValue();
this.managerBuilder.add(new ServletPatternRequestMatcher(entry.getKey()),
registry.authorizationManager());
}
return postProcess(this.managerBuilder.build());
}
private AuthorizationManager<HttpServletRequest> authorizationManager() {
Assert.state(this.unmappedMatchers == null, Assert.state(this.unmappedMatchers == null,
() -> "An incomplete mapping was found for " + this.unmappedMatchers () -> "An incomplete mapping was found for " + this.unmappedMatchers
+ ". Try completing it with something like requestUrls().<something>.hasRole('USER')"); + ". Try completing it with something like requestUrls().<something>.hasRole('USER')");
Assert.state(this.mappingCount > 0, Assert.state(this.mappingCount > 0,
"At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())");
return postProcess(this.managerBuilder.build());
}
private AuthorizationManager<HttpServletRequest> createAuthorizationManager() {
AuthorizationManager<HttpServletRequest> manager = (this.servletPattern.isEmpty()) ? authorizationManager()
: servletAuthorizationManager();
ObservationRegistry registry = getObservationRegistry(); ObservationRegistry registry = getObservationRegistry();
RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build());
if (registry.isNoop()) { if (registry.isNoop()) {
return manager; return manager;
} }
@ -179,9 +209,77 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
} }
@Override @Override
protected AuthorizedUrl chainRequestMatchers(List<RequestMatcher> requestMatchers) { protected AuthorizedUrl<AuthorizationManagerRequestMatcherRegistry> chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
this.unmappedMatchers = requestMatchers; this.unmappedMatchers = requestMatchers;
return new AuthorizedUrl(requestMatchers); return new AuthorizedUrl<>(
(manager) -> AuthorizeHttpRequestsConfigurer.this.addMapping(requestMatchers, manager));
}
/**
* Begin registering {@link RequestMatcher}s based on the type of the servlet
* mapped to {@code pattern}. Each registered request matcher will additionally
* check {@link HttpServletMapping#getPattern} against the provided
* {@code pattern}.
*
* <p>
* If the corresponding servlet is of type {@link DispatcherServlet}, then use a
* {@link AuthorizationManagerServletRequestMatcherRegistry} that registers
* {@link MvcRequestMatcher}s.
*
* <p>
* Otherwise, use a configurer that registers {@link AntPathRequestMatcher}s.
*
* <p>
* When doing a path-based pattern, like `/path/*`, registered URIs should leave
* out the matching path. For example, if the target URI is `/path/resource/3`,
* then the configuration should look like this: <code>
* .forServletPattern("/path/*", (path) -> path
* .requestMatchers("/resource/3").hasAuthority(...)
* )
* </code>
*
* <p>
* Or, if the pattern is `/path/subpath/*`, and the URI is
* `/path/subpath/resource/3`, then the configuration should look like this:
* <code>
* .forServletPattern("/path/subpath/*", (path) -> path
* .requestMatchers("/resource/3").hasAuthority(...)
* )
* </code>
*
* <p>
* For all other patterns, please supply the URI in absolute terms. For example,
* if the target URI is `/js/**` and it matches to the default servlet, then the
* configuration should look like this: <code>
* .forServletPattern("/", (root) -> root
* .requestMatchers("/js/**").hasAuthority(...)
* )
* </code>
*
* <p>
* Or, if the target URI is `/views/**`, and it matches to a `*.jsp` extension
* servlet, then the configuration should look like this: <code>
* .forServletPattern("*.jsp", (jsp) -> jsp
* .requestMatchers("/views/**").hasAuthority(...)
* )
* </code>
* @param customizer a customizer that uses a
* {@link AuthorizationManagerServletRequestMatcherRegistry} for URIs mapped to
* the provided servlet
* @return an {@link AuthorizationManagerServletRequestMatcherRegistry} for
* further configurations
* @since 6.2
*/
public AuthorizationManagerRequestMatcherRegistry forServletPattern(String pattern,
Customizer<AuthorizationManagerServletRequestMatcherRegistry> customizer) {
ApplicationContext context = getApplicationContext();
RequestMatcherBuilder builder = RequestMatcherBuilders.createForServletPattern(context, pattern);
AuthorizationManagerServletRequestMatcherRegistry registry = new AuthorizationManagerServletRequestMatcherRegistry(
builder);
customizer.customize(registry);
this.servletPattern.put(pattern, registry);
return this;
} }
/** /**
@ -237,6 +335,125 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
return AuthorizeHttpRequestsConfigurer.this.and(); return AuthorizeHttpRequestsConfigurer.this.and();
} }
/**
* A decorator class for registering {@link RequestMatcher} instances based on the
* type of servlet. If the servlet is {@link DispatcherServlet}, then it will use
* a {@link MvcRequestMatcher}; otherwise, it will use a
* {@link AntPathRequestMatcher}.
*
* <p>
* This class is designed primarily for use with the {@link HttpSecurity} DSL. For
* that reason, please use {@link HttpSecurity#authorizeHttpRequests} instead as
* it exposes this class fluently alongside related DSL configurations.
*
* <p>
* NOTE: In many cases, which kind of request matcher is needed is apparent by the
* servlet configuration, and so you should generally use the methods found in
* {@link AbstractRequestMatcherRegistry} instead of this these. Use this class
* when you want or need to indicate which request matcher URIs belong to which
* servlet.
*
* <p>
* In all cases, though, you may arrange your request matchers by servlet pattern
* with the {@link AuthorizationManagerRequestMatcherRegistry#forServletPattern}
* method in the {@link HttpSecurity#authorizeHttpRequests} DSL.
*
* <p>
* Consider, for example, the circumstance where you have Spring MVC configured
* and also Spring Boot H2 Console. Spring MVC registers a servlet of type
* {@link DispatcherServlet} as the default servlet and Spring Boot registers a
* servlet of its own as well at `/h2-console/*`.
*
* <p>
* Such might have a configuration like this in Spring Security: <code>
* http
* .authorizeHttpRequests((authorize) -> authorize
* .requestMatchers("/js/**", "/css/**").permitAll()
* .requestMatchers("/my/controller/**").hasAuthority("CONTROLLER")
* .requestMatchers("/h2-console/**").hasAuthority("H2")
* )
* // ...
* </code>
*
* <p>
* Spring Security by default addresses the above configuration on its own.
*
* <p>
* However, consider the same situation, but where {@link DispatcherServlet} is
* mapped to a path like `/mvc/*`. In this case, the above configuration is
* ambiguous, and you should use this class to clarify the rest of each MVC URI
* like so: <code>
* http
* .authorizeHttpRequests((authorize) -> authorize
* .forServletPattern("/", (root) -> root
* .requestMatchers("/js/**", "/css/**").permitAll()
* )
* .forServletPattern("/mvc/*", (mvc) -> mvc
* .requestMatchers("/my/controller/**").hasAuthority("CONTROLLER")
* )
* .forServletPattern("/h2-console/*", (h2) -> h2
* .anyRequest().hasAuthority("OTHER")
* )
* )
* // ...
* </code>
*
* <p>
* In the above configuration, it's now clear to Spring Security that the
* following matchers map to these corresponding URIs:
*
* <ul>
* <li>&lt;default&gt; + <strong>`/js/**`</strong> ==> `/js/**`</li>
* <li>&lt;default&gt; + <strong>`/css/**`</strong> ==> `/css/**`</li>
* <li>`/mvc` + <strong>`/my/controller/**`</strong> ==>
* `/mvc/my/controller/**`</li>
* <li>`/h2-console` + <strong>&lt;any request&gt;</strong> ==>
* `/h2-console/**`</li>
* </ul>
*
* @author Josh Cummings
* @since 6.2
* @see AbstractRequestMatcherRegistry
* @see AuthorizeHttpRequestsConfigurer
*/
public final class AuthorizationManagerServletRequestMatcherRegistry extends
AbstractRequestMatcherBuilderRegistry<AuthorizedUrl<AuthorizationManagerServletRequestMatcherRegistry>> {
private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager
.builder();
private List<RequestMatcher> unmappedMatchers;
AuthorizationManagerServletRequestMatcherRegistry(RequestMatcherBuilder builder) {
super(AuthorizationManagerRequestMatcherRegistry.this.getApplicationContext(), builder);
}
AuthorizationManager<RequestAuthorizationContext> authorizationManager() {
Assert.state(this.unmappedMatchers == null,
() -> "An incomplete mapping was found for " + this.unmappedMatchers
+ ". Try completing it with something like requestUrls().<something>.hasRole('USER')");
AuthorizationManager<HttpServletRequest> request = this.managerBuilder.build();
return (authentication, context) -> request.check(authentication, context.getRequest());
}
@Override
protected AuthorizedUrl<AuthorizationManagerServletRequestMatcherRegistry> chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
this.unmappedMatchers = requestMatchers;
return new AuthorizedUrl<>((manager) -> addMapping(requestMatchers, manager));
}
private AuthorizationManagerServletRequestMatcherRegistry addMapping(List<RequestMatcher> matchers,
AuthorizationManager<RequestAuthorizationContext> manager) {
this.unmappedMatchers = null;
for (RequestMatcher matcher : matchers) {
this.managerBuilder.add(matcher, manager);
}
return this;
}
}
} }
/** /**
@ -245,20 +462,12 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* *
* @author Evgeniy Cheban * @author Evgeniy Cheban
*/ */
public class AuthorizedUrl { public class AuthorizedUrl<R> {
private final List<? extends RequestMatcher> matchers; private final Function<AuthorizationManager<RequestAuthorizationContext>, R> registrar;
/** AuthorizedUrl(Function<AuthorizationManager<RequestAuthorizationContext>, R> registrar) {
* Creates an instance. this.registrar = registrar;
* @param matchers the {@link RequestMatcher} instances to map
*/
AuthorizedUrl(List<? extends RequestMatcher> matchers) {
this.matchers = matchers;
}
protected List<? extends RequestMatcher> getMatchers() {
return this.matchers;
} }
/** /**
@ -266,7 +475,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry permitAll() { public R permitAll() {
return access(permitAllAuthorizationManager); return access(permitAllAuthorizationManager);
} }
@ -275,7 +484,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry denyAll() { public R denyAll() {
return access((a, o) -> new AuthorizationDecision(false)); return access((a, o) -> new AuthorizationDecision(false));
} }
@ -286,7 +495,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return {@link AuthorizationManagerRequestMatcherRegistry} for further * @return {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry hasRole(String role) { public R hasRole(String role) {
return access(withRoleHierarchy(AuthorityAuthorizationManager return access(withRoleHierarchy(AuthorityAuthorizationManager
.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role }))); .hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })));
} }
@ -299,7 +508,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) { public R hasAnyRole(String... roles) {
return access(withRoleHierarchy( return access(withRoleHierarchy(
AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles))); AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles)));
} }
@ -310,7 +519,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) { public R hasAuthority(String authority) {
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority))); return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)));
} }
@ -321,7 +530,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) { public R hasAnyAuthority(String... authorities) {
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities))); return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
} }
@ -336,7 +545,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry authenticated() { public R authenticated() {
return access(AuthenticatedAuthorizationManager.authenticated()); return access(AuthenticatedAuthorizationManager.authenticated());
} }
@ -348,7 +557,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @since 5.8 * @since 5.8
* @see RememberMeConfigurer * @see RememberMeConfigurer
*/ */
public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() { public R fullyAuthenticated() {
return access(AuthenticatedAuthorizationManager.fullyAuthenticated()); return access(AuthenticatedAuthorizationManager.fullyAuthenticated());
} }
@ -359,7 +568,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @since 5.8 * @since 5.8
* @see RememberMeConfigurer * @see RememberMeConfigurer
*/ */
public AuthorizationManagerRequestMatcherRegistry rememberMe() { public R rememberMe() {
return access(AuthenticatedAuthorizationManager.rememberMe()); return access(AuthenticatedAuthorizationManager.rememberMe());
} }
@ -369,7 +578,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* customization * customization
* @since 5.8 * @since 5.8
*/ */
public AuthorizationManagerRequestMatcherRegistry anonymous() { public R anonymous() {
return access(AuthenticatedAuthorizationManager.anonymous()); return access(AuthenticatedAuthorizationManager.anonymous());
} }
@ -379,10 +588,9 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customizations * customizations
*/ */
public AuthorizationManagerRequestMatcherRegistry access( public R access(AuthorizationManager<RequestAuthorizationContext> manager) {
AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.notNull(manager, "manager cannot be null"); Assert.notNull(manager, "manager cannot be null");
return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager); return this.registrar.apply(manager);
} }
} }

View File

@ -0,0 +1,103 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
final class DispatcherServletDelegatingRequestMatcherBuilder implements RequestMatcherBuilder {
final MvcRequestMatcherBuilder mvc;
final AntPathRequestMatcherBuilder ant;
final ServletRegistrationCollection registrations;
DispatcherServletDelegatingRequestMatcherBuilder(MvcRequestMatcherBuilder mvc, AntPathRequestMatcherBuilder ant,
ServletRegistrationCollection registrations) {
this.mvc = mvc;
this.ant = ant;
this.registrations = registrations;
}
@Override
public RequestMatcher matcher(String pattern) {
MvcRequestMatcher mvc = this.mvc.matcher(pattern);
AntPathRequestMatcher ant = this.ant.matcher(pattern);
return new DispatcherServletDelegatingRequestMatcher(mvc, ant, this.registrations);
}
@Override
public RequestMatcher matcher(HttpMethod method, String pattern) {
MvcRequestMatcher mvc = this.mvc.matcher(method, pattern);
AntPathRequestMatcher ant = this.ant.matcher(method, pattern);
return new DispatcherServletDelegatingRequestMatcher(mvc, ant, this.registrations);
}
static final class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
private final MvcRequestMatcher mvc;
private final AntPathRequestMatcher ant;
private final ServletRegistrationCollection registrations;
private DispatcherServletDelegatingRequestMatcher(MvcRequestMatcher mvc, AntPathRequestMatcher ant,
ServletRegistrationCollection registrations) {
this.mvc = mvc;
this.ant = ant;
this.registrations = registrations;
}
@Override
public boolean matches(HttpServletRequest request) {
String name = request.getHttpServletMapping().getServletName();
ServletRegistrationCollection.Registration registration = this.registrations.registrationByName(name);
Assert.notNull(registration,
String.format("Could not find %s in servlet configuration %s", name, this.registrations));
if (registration.isDispatcherServlet()) {
return this.mvc.matches(request);
}
return this.ant.matches(request);
}
@Override
public MatchResult matcher(HttpServletRequest request) {
String name = request.getHttpServletMapping().getServletName();
ServletRegistrationCollection.Registration registration = this.registrations.registrationByName(name);
Assert.notNull(registration,
String.format("Could not find %s in servlet configuration %s", name, this.registrations));
if (registration.isDispatcherServlet()) {
return this.mvc.matcher(request);
}
return this.ant.matcher(request);
}
@Override
public String toString() {
return String.format("DispatcherServlet [mvc=[%s], ant=[%s], servlet=[%s]]", this.mvc, this.ant,
this.registrations);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
final class MvcRequestMatcherBuilder implements RequestMatcherBuilder {
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
private final HandlerMappingIntrospector introspector;
private final ObjectPostProcessor<Object> objectPostProcessor;
private final String servletPath;
private MvcRequestMatcherBuilder(ApplicationContext context, String servletPath) {
if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME
+ " of type " + HandlerMappingIntrospector.class.getName()
+ " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.");
}
this.introspector = context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class);
this.objectPostProcessor = context.getBean(ObjectPostProcessor.class);
this.servletPath = servletPath;
}
static MvcRequestMatcherBuilder absolute(ApplicationContext context) {
return new MvcRequestMatcherBuilder(context, null);
}
static MvcRequestMatcherBuilder relativeTo(ApplicationContext context, String path) {
return new MvcRequestMatcherBuilder(context, path);
}
@Override
public MvcRequestMatcher matcher(String pattern) {
MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern);
this.objectPostProcessor.postProcess(matcher);
if (this.servletPath != null) {
matcher.setServletPath(this.servletPath);
}
return matcher;
}
@Override
public MvcRequestMatcher matcher(HttpMethod method, String pattern) {
MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern);
this.objectPostProcessor.postProcess(matcher);
matcher.setMethod(method);
if (this.servletPath != null) {
matcher.setServletPath(this.servletPath);
}
return matcher;
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* An interface that abstracts how matchers are created
*
* @author Josh Cummings
* @since 6.2
*/
interface RequestMatcherBuilder {
/**
* Create a request matcher for the given pattern.
*
* <p>
* For example, you might do something like the following: <code>
* builder.matcher("/controller/**")
* </code>
* @param pattern the pattern to use, typically an Ant path
* @return a {@link RequestMatcher} that matches on the given {@code pattern}
*/
RequestMatcher matcher(String pattern);
/**
* Create a request matcher for the given pattern.
*
* <p>
* For example, you might do something like the following: <code>
* builder.matcher(HttpMethod.GET, "/controller/**")
* </code>
* @param method the HTTP method to use
* @param pattern the pattern to use, typically an Ant path
* @return a {@link RequestMatcher} that matches on the given HTTP {@code method} and
* {@code pattern}
*/
RequestMatcher matcher(HttpMethod method, String pattern);
/**
* Create a request matcher that matches any request
* @return a {@link RequestMatcher} that matches any request
*/
default RequestMatcher any() {
return AnyRequestMatcher.INSTANCE;
}
/**
* Create an array request matchers, one for each of the given patterns.
*
* <p>
* For example, you might do something like the following: <code>
* builder.matcher("/controller-one/**", "/controller-two/**")
* </code>
* @param patterns the patterns to use, typically Ant paths
* @return a list of {@link RequestMatcher} that match on the given {@code pattern}
*/
default List<RequestMatcher> matchers(String... patterns) {
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
matchers.add(matcher(pattern));
}
return matchers;
}
/**
* Create an array request matchers, one for each of the given patterns.
*
* <p>
* For example, you might do something like the following: <code>
* builder.matcher(HttpMethod.POST, "/controller-one/**", "/controller-two/**")
* </code>
* @param method the HTTP method to use
* @param patterns the patterns to use, typically Ant paths
* @return a list of {@link RequestMatcher} that match on the given HTTP
* {@code method} and {@code pattern}
*/
default List<RequestMatcher> matchers(HttpMethod method, String... patterns) {
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
matchers.add(matcher(method, pattern));
}
return matchers;
}
}

View File

@ -0,0 +1,215 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.servlet.DispatcherServlet;
/**
* A factory for constructing {@link RequestMatcherBuilder} instances
*
* @author Josh Cummings
* @since 6.2
*/
final class RequestMatcherBuilders {
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
private static final boolean mvcPresent;
static {
mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, RequestMatcherBuilders.class.getClassLoader());
}
private static final Log logger = LogFactory.getLog(RequestMatcherBuilders.class);
private RequestMatcherBuilders() {
}
/**
* Create the default {@link RequestMatcherBuilder} for use by Spring Security DSLs.
*
* <p>
* If Spring MVC is not present on the classpath or if there is no
* {@link DispatcherServlet}, this method will return an Ant-based builder.
*
* <p>
* If the servlet configuration has only {@link DispatcherServlet} with a single
* mapping (for example `/` or `/path/*`), then this method will return an MVC-based
* builder.
*
* <p>
* If the servlet configuration maps {@link DispatcherServlet} to a path and also has
* other servlets, this will throw an exception. In that case, an application should
* instead use the {@link RequestMatcherBuilders#createForServletPattern} ideally with
* the associated
* {@link org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer}
* to create builders by servlet path.
*
* <p>
* Otherwise, (namely if {@link DispatcherServlet} is root), this method will return a
* builder that delegates to an Ant or Mvc builder at runtime.
* @param context the application context
* @return the appropriate {@link RequestMatcherBuilder} based on application
* configuration
*/
static RequestMatcherBuilder createDefault(ApplicationContext context) {
if (!mvcPresent) {
logger.trace("Defaulting to Ant matching since Spring MVC is not on the classpath");
return AntPathRequestMatcherBuilder.absolute();
}
if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
logger.trace("Defaulting to Ant matching since Spring MVC is not fully configured");
return AntPathRequestMatcherBuilder.absolute();
}
ServletRegistrationCollection registrations = ServletRegistrationCollection.registrations(context);
if (registrations.isEmpty()) {
logger.trace("Defaulting to MVC matching since Spring MVC is on the class path and no servlet "
+ "information is available");
return AntPathRequestMatcherBuilder.absolute();
}
ServletRegistrationCollection dispatcherServlets = registrations.dispatcherServlets();
if (dispatcherServlets.isEmpty()) {
logger.trace("Defaulting to Ant matching since there is no DispatcherServlet configured");
return AntPathRequestMatcherBuilder.absolute();
}
ServletRegistrationCollection.ServletPath servletPath = registrations.deduceOneServletPath();
if (servletPath != null) {
String message = "Defaulting to MVC matching since DispatcherServlet [%s] is the only servlet mapping";
logger.trace(String.format(message, servletPath.path()));
return MvcRequestMatcherBuilder.relativeTo(context, servletPath.path());
}
servletPath = dispatcherServlets.deduceOneServletPath();
if (servletPath == null) {
logger.trace("Did not choose a default since there is more than one DispatcherServlet mapping");
String message = String.format("""
This method cannot decide whether these patterns are Spring MVC patterns or not
since your servlet configuration has multiple Spring MVC servlet mappings.
For your reference, here is your servlet configuration: %s
To address this, you need to specify the servlet path for each endpoint.
You can use .forServletPattern in conjunction with requestMatchers do to this
like so:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.forServletPattern("/mvc-one/*", (one) -> one
.requestMatchers("/controller/**", "/endpoints/**"
)...
.forServletPattern("/mvc-two/*", (two) -> two
.requestMatchers("/other/**", "/controllers/**")...
)
.forServletPattern("/h2-console/*", (h2) -> h2
.requestMatchers("/**")...
)
)
// ...
return http.build();
}
""", registrations);
return new ErrorRequestMatcherBuilder(message);
}
if (servletPath.path() != null) {
logger.trace("Did not choose a default since there is a non-root DispatcherServlet mapping");
String message = String.format("""
This method cannot decide whether these patterns are Spring MVC patterns or not
since your Spring MVC mapping is mapped to a path and you have other servlet mappings.
For your reference, here is your servlet configuration: %s
To address this, you need to specify the servlet path for each endpoint.
You can use .forServletPattern in conjunction with requestMatchers do to this
like so:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.forServletPattern("/mvc/*", (mvc) -> mvc
.requestMatchers("/controller/**", "/endpoints/**")...
)
.forServletPattern("/h2-console/*", (h2) -> h2
.requestMatchers("/**")...
)
)
// ...
return http.build();
}
""", registrations);
return new ErrorRequestMatcherBuilder(message);
}
logger.trace("Defaulting to request-time checker since DispatcherServlet is mapped to root, but there are also "
+ "other servlet mappings");
return new DispatcherServletDelegatingRequestMatcherBuilder(MvcRequestMatcherBuilder.absolute(context),
AntPathRequestMatcherBuilder.absolute(), registrations);
}
static RequestMatcherBuilder createForServletPattern(ApplicationContext context, String pattern) {
Assert.notNull(pattern, "pattern cannot be null");
ServletRegistrationCollection registrations = ServletRegistrationCollection.registrations(context);
ServletRegistrationCollection.Registration registration = registrations.registrationByMapping(pattern);
Assert.notNull(registration, () -> String
.format("The given pattern %s doesn't seem to match any configured servlets: %s", pattern, registrations));
boolean isPathPattern = pattern.startsWith("/") && pattern.endsWith("/*");
if (isPathPattern) {
String path = pattern.substring(0, pattern.length() - 2);
return (registration.isDispatcherServlet()) ? MvcRequestMatcherBuilder.relativeTo(context, path)
: AntPathRequestMatcherBuilder.relativeTo(path);
}
return (registration.isDispatcherServlet()) ? MvcRequestMatcherBuilder.absolute(context)
: AntPathRequestMatcherBuilder.absolute();
}
private static class ErrorRequestMatcherBuilder implements RequestMatcherBuilder {
private final String errorMessage;
ErrorRequestMatcherBuilder(String errorMessage) {
this.errorMessage = errorMessage;
}
@Override
public RequestMatcher matcher(String pattern) {
throw new IllegalArgumentException(this.errorMessage);
}
@Override
public RequestMatcher matcher(HttpMethod method, String pattern) {
throw new IllegalArgumentException(this.errorMessage);
}
@Override
public RequestMatcher any() {
throw new IllegalArgumentException(this.errorMessage);
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
final class ServletPatternRequestMatcher implements RequestMatcher {
final String pattern;
ServletPatternRequestMatcher(String pattern) {
Assert.notNull(pattern, "pattern cannot be null");
this.pattern = pattern;
}
@Override
public boolean matches(HttpServletRequest request) {
return this.pattern.equals(request.getHttpServletMapping().getPattern());
}
@Override
public String toString() {
return String.format("ServletPattern [pattern='%s']", this.pattern);
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.WebApplicationContext;
final class ServletRegistrationCollection {
private List<Registration> registrations;
private ServletRegistrationCollection() {
this.registrations = Collections.emptyList();
}
private ServletRegistrationCollection(List<Registration> registrations) {
this.registrations = registrations;
}
static ServletRegistrationCollection registrations(ApplicationContext context) {
if (!(context instanceof WebApplicationContext web)) {
return new ServletRegistrationCollection();
}
ServletContext servletContext = web.getServletContext();
if (servletContext == null) {
return new ServletRegistrationCollection();
}
Map<String, ? extends ServletRegistration> registrations = servletContext.getServletRegistrations();
if (registrations == null) {
return new ServletRegistrationCollection();
}
List<Registration> filtered = new ArrayList<>();
for (ServletRegistration registration : registrations.values()) {
Collection<String> mappings = registration.getMappings();
if (!CollectionUtils.isEmpty(mappings)) {
filtered.add(new Registration(registration));
}
}
return new ServletRegistrationCollection(filtered);
}
boolean isEmpty() {
return this.registrations.isEmpty();
}
Registration registrationByName(String name) {
for (Registration registration : this.registrations) {
if (registration.registration().getName().equals(name)) {
return registration;
}
}
return null;
}
Registration registrationByMapping(String target) {
for (Registration registration : this.registrations) {
for (String mapping : registration.registration().getMappings()) {
if (target.equals(mapping)) {
return registration;
}
}
}
return null;
}
ServletRegistrationCollection dispatcherServlets() {
List<Registration> dispatcherServlets = new ArrayList<>();
for (Registration registration : this.registrations) {
if (registration.isDispatcherServlet()) {
dispatcherServlets.add(registration);
}
}
return new ServletRegistrationCollection(dispatcherServlets);
}
ServletPath deduceOneServletPath() {
if (this.registrations.size() > 1) {
return null;
}
ServletRegistration registration = this.registrations.iterator().next().registration();
if (registration.getMappings().size() > 1) {
return null;
}
String mapping = registration.getMappings().iterator().next();
if ("/".equals(mapping)) {
return new ServletPath();
}
if (mapping.endsWith("/*")) {
return new ServletPath(mapping.substring(0, mapping.length() - 2));
}
return null;
}
@Override
public String toString() {
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
for (Registration registration : this.registrations) {
mappings.put(registration.registration().getClassName(), registration.registration().getMappings());
}
return mappings.toString();
}
record Registration(ServletRegistration registration) {
boolean isDispatcherServlet() {
Class<?> dispatcherServlet = ClassUtils
.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
try {
Class<?> clazz = Class.forName(this.registration.getClassName());
if (dispatcherServlet.isAssignableFrom(clazz)) {
return true;
}
}
catch (ClassNotFoundException ex) {
return false;
}
return false;
}
}
record ServletPath(String path) {
ServletPath() {
this(null);
}
}
}

View File

@ -25,8 +25,12 @@ import org.springframework.http.HttpMethod;
import org.springframework.security.test.support.ClassPathExclusions; import org.springframework.security.test.support.ClassPathExclusions;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link AbstractRequestMatcherRegistry} with no Spring MVC in the classpath * Tests for {@link AbstractRequestMatcherRegistry} with no Spring MVC in the classpath
@ -41,6 +45,9 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
this.matcherRegistry = new TestRequestMatcherRegistry(); this.matcherRegistry = new TestRequestMatcherRegistry();
WebApplicationContext context = mock(WebApplicationContext.class);
given(context.getBeanNamesForType((Class<?>) any())).willReturn(new String[0]);
this.matcherRegistry.setApplicationContext(context);
} }
@Test @Test

View File

@ -163,12 +163,6 @@ public class AbstractRequestMatcherRegistryTests {
assertThat(requestMatchers).isNotEmpty(); assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1); assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
servletContext.addServlet("servletOne", Servlet.class);
servletContext.addServlet("servletTwo", Servlet.class);
requestMatchers = this.matcherRegistry.requestMatchers("/**");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
} }
@Test @Test

View File

@ -0,0 +1,349 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.ObjectAssert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AbstractRequestMatcherBuilderRegistry}
*/
class AbstractRequestMatcherBuilderRegistryTests {
@Test
void defaultServletMatchersWhenDefaultDispatcherServletThenMvc() {
MockServletContext servletContext = MockServletContext.mvc();
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/mvc").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/mvc");
assertThatMvc(matchers).method().isNull();
}
@Test
void defaultServletHttpMethodMatchersWhenDefaultDispatcherServletThenMvc() {
MockServletContext servletContext = MockServletContext.mvc();
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers(HttpMethod.GET, "/mvc").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/mvc");
assertThatMvc(matchers).method().isEqualTo(HttpMethod.GET);
}
@Test
void servletMatchersWhenPathDispatcherServletThenMvc() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*")
.requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
assertThatMvc(matchers).pattern().isEqualTo("/controller");
}
@Test
void servletMatchersWhenAlsoExtraServletContainerMappingsThenMvc() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class);
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*")
.requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
assertThatMvc(matchers).pattern().isEqualTo("/controller");
}
@Test
void defaultServletMatchersWhenOnlyDefaultServletThenAnt() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
assertThatAnt(matchers).pattern().isEqualTo("/controller");
}
@Test
void defaultDispatcherServletMatchersWhenNoHandlerMappingIntrospectorThenException() {
MockServletContext servletContext = MockServletContext.mvc();
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> defaultServlet(servletContext, (context) -> {
}));
}
@Test
void dispatcherServletMatchersWhenNoHandlerMappingIntrospectorThenException() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> servletPattern(servletContext, (context) -> {
}, "/mvc/*"));
}
@Test
void matchersWhenNoDispatchServletThenAnt() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/services/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint");
}
@Test
void servletMatchersWhenMixedServletsThenDeterminesByServletPath() {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
List<RequestMatcher> matchers = servletPattern(servletContext, "/services/*")
.requestMatchers("/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint");
matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/controller");
}
@Test
void servletMatchersWhenDispatcherServletNotDefaultThenDeterminesByServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*")
.requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
assertThatMvc(matchers).pattern().isEqualTo("/controller");
matchers = defaultServlet(servletContext).requestMatchers("/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
assertThatAnt(matchers).pattern().isEqualTo("/endpoint");
}
@Test
void servletHttpMatchersWhenDispatcherServletNotDefaultThenDeterminesByServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*").requestMatchers(HttpMethod.GET,
"/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).method().isEqualTo(HttpMethod.GET);
assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
assertThatMvc(matchers).pattern().isEqualTo("/controller");
matchers = defaultServlet(servletContext).requestMatchers(HttpMethod.GET, "/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
assertThatAnt(matchers).method().isEqualTo(HttpMethod.GET);
assertThatAnt(matchers).pattern().isEqualTo("/endpoint");
}
@Test
void servletMatchersWhenTwoDispatcherServletsThenDeterminesByServletPath() {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("two", DispatcherServlet.class).addMapping("/other/*");
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/controller");
matchers = servletPattern(servletContext, "/other/*").requestMatchers("/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isEqualTo("/other");
assertThatMvc(matchers).pattern().isEqualTo("/endpoint");
}
@Test
void servletMatchersWhenMoreThanOneMappingThenDeterminesByServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/controller");
matchers = servletPattern(servletContext, "/two/*").requestMatchers("/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isEqualTo("/two");
assertThatMvc(matchers).pattern().isEqualTo("/endpoint");
}
@Test
void servletMatchersWhenMoreThanOneMappingAndDefaultServletsThenDeterminesByServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/controller");
matchers = servletPattern(servletContext, "/two/*").requestMatchers("/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isEqualTo("/two");
assertThatMvc(matchers).pattern().isEqualTo("/endpoint");
}
@Test
void defaultServletWhenDispatcherServletThenMvc() {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
assertThatMvc(matchers).servletPath().isNull();
assertThatMvc(matchers).pattern().isEqualTo("/controller");
matchers = servletPattern(servletContext, "/services/*").requestMatchers("/endpoint").matchers;
assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint");
}
@Test
void defaultServletWhenNoDefaultServletThenException() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> defaultServlet(servletContext));
}
@Test
void servletPathWhenNoMatchingServletThenException() {
MockServletContext servletContext = MockServletContext.mvc();
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> servletPattern(servletContext, "/wrong/*"));
}
TestServletRequestMatcherRegistry defaultServlet(ServletContext servletContext) {
return servletPattern(servletContext, "/");
}
TestServletRequestMatcherRegistry defaultServlet(ServletContext servletContext,
Consumer<GenericWebApplicationContext> consumer) {
return servletPattern(servletContext, consumer, "/");
}
TestServletRequestMatcherRegistry servletPattern(ServletContext servletContext, String pattern) {
return servletPattern(servletContext, (context) -> {
context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class);
context.registerBean(ObjectPostProcessor.class, () -> mock(ObjectPostProcessor.class));
}, pattern);
}
TestServletRequestMatcherRegistry servletPattern(ServletContext servletContext,
Consumer<GenericWebApplicationContext> consumer, String pattern) {
GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext);
consumer.accept(context);
context.refresh();
return new TestServletRequestMatcherRegistry(context, pattern);
}
static MvcRequestMatcherAssert assertThatMvc(List<RequestMatcher> matchers) {
RequestMatcher matcher = matchers.get(0);
if (matcher instanceof AndRequestMatcher matching) {
List<RequestMatcher> and = (List<RequestMatcher>) ReflectionTestUtils.getField(matching, "requestMatchers");
assertThat(and).hasSize(2);
assertThat(and.get(1)).isInstanceOf(MvcRequestMatcher.class);
return new MvcRequestMatcherAssert((MvcRequestMatcher) and.get(1));
}
assertThat(matcher).isInstanceOf(MvcRequestMatcher.class);
return new MvcRequestMatcherAssert((MvcRequestMatcher) matcher);
}
static AntPathRequestMatcherAssert assertThatAnt(List<RequestMatcher> matchers) {
RequestMatcher matcher = matchers.get(0);
if (matcher instanceof AndRequestMatcher matching) {
List<RequestMatcher> and = (List<RequestMatcher>) ReflectionTestUtils.getField(matching, "requestMatchers");
assertThat(and).hasSize(2);
assertThat(and.get(1)).isInstanceOf(AntPathRequestMatcher.class);
return new AntPathRequestMatcherAssert((AntPathRequestMatcher) and.get(1));
}
assertThat(matcher).isInstanceOf(AntPathRequestMatcher.class);
return new AntPathRequestMatcherAssert((AntPathRequestMatcher) matcher);
}
static final class TestServletRequestMatcherRegistry
extends AbstractRequestMatcherBuilderRegistry<TestServletRequestMatcherRegistry> {
List<RequestMatcher> matchers;
TestServletRequestMatcherRegistry(ApplicationContext context, String pattern) {
super(context, RequestMatcherBuilders.createForServletPattern(context, pattern));
}
@Override
protected TestServletRequestMatcherRegistry chainRequestMatchers(List<RequestMatcher> requestMatchers) {
this.matchers = requestMatchers;
return this;
}
}
static final class MvcRequestMatcherAssert extends ObjectAssert<MvcRequestMatcher> {
private MvcRequestMatcherAssert(MvcRequestMatcher matcher) {
super(matcher);
}
AbstractObjectAssert<?, ?> servletPath() {
return extracting("servletPath");
}
AbstractObjectAssert<?, ?> pattern() {
return extracting("pattern");
}
AbstractObjectAssert<?, ?> method() {
return extracting("method");
}
}
static final class AntPathRequestMatcherAssert extends ObjectAssert<AntPathRequestMatcher> {
private AntPathRequestMatcherAssert(AntPathRequestMatcher matcher) {
super(matcher);
}
AbstractObjectAssert<?, ?> pattern() {
return extracting("pattern");
}
AbstractObjectAssert<?, ?> method() {
return extracting("httpMethod");
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.configurers;
import java.util.function.Supplier; import java.util.function.Supplier;
import jakarta.servlet.Servlet;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -26,6 +27,7 @@ import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.RememberMeAuthenticationToken;
@ -33,6 +35,7 @@ import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -58,6 +61,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
@ -71,6 +75,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -121,7 +126,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
public void configureWhenMvcMatcherAfterAnyRequestThenException() { public void configureWhenMvcMatcherAfterAnyRequestThenException() {
assertThatExceptionOfType(BeanCreationException.class) assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(AfterAnyRequestConfig.class).autowire()) .isThrownBy(() -> this.spring.register(AfterAnyRequestConfig.class).autowire())
.withMessageContaining("Can't configure mvcMatchers after anyRequest"); .withMessageContaining("Can't configure requestMatchers after anyRequest");
} }
@Test @Test
@ -362,7 +367,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
@Test @Test
public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserThenRespondsWithForbidden() throws Exception { public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserThenRespondsWithForbidden() throws Exception {
this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire();
// @formatter:off // @formatter:off
MockHttpServletRequestBuilder requestWithUser = get("/spring/") MockHttpServletRequestBuilder requestWithUser = get("/spring/")
.servletPath("/spring") .servletPath("/spring")
@ -375,7 +380,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
@Test @Test
public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserAndWithoutServletPathThenRespondsWithForbidden() public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserAndWithoutServletPathThenRespondsWithForbidden()
throws Exception { throws Exception {
this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire();
// @formatter:off // @formatter:off
MockHttpServletRequestBuilder requestWithUser = get("/") MockHttpServletRequestBuilder requestWithUser = get("/")
.with(user("user") .with(user("user")
@ -386,7 +391,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
@Test @Test
public void getWhenServletPathRoleAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception { public void getWhenServletPathRoleAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception {
this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire();
// @formatter:off // @formatter:off
MockHttpServletRequestBuilder requestWithAdmin = get("/spring/") MockHttpServletRequestBuilder requestWithAdmin = get("/spring/")
.servletPath("/spring") .servletPath("/spring")
@ -596,6 +601,200 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
} }
@Test
public void configureWhenNoDispatcherServletThenSucceeds() throws Exception {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
this.spring.register(AuthorizeHttpRequestsConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/path")).andExpect(status().isNotFound());
}
@Test
public void configureWhenOnlyDispatcherServletThenSucceeds() throws Exception {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
this.spring.register(AuthorizeHttpRequestsConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/mvc/path").servletPath("/mvc")).andExpect(status().isNotFound());
this.mvc.perform(get("/mvc")).andExpect(status().isUnauthorized());
}
@Test
public void configureWhenMultipleServletsThenSucceeds() throws Exception {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
this.spring.register(AuthorizeHttpRequestsConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/path").with(servletPath("/path"))).andExpect(status().isNotFound());
}
@Test
public void configureWhenAmbiguousServletsThenWiringException() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(AuthorizeHttpRequestsConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire());
}
@Test
void defaultServletMatchersWhenDefaultServletThenPermits() throws Exception {
this.spring.register(DefaultServletConfig.class)
.postProcessor((context) -> context.setServletContext(MockServletContext.mvc()))
.autowire();
this.mvc.perform(get("/path/path").with(defaultServlet())).andExpect(status().isNotFound());
this.mvc.perform(get("/path/path").with(servletPath("/path"))).andExpect(status().isUnauthorized());
}
@Test
void defaultServletHttpMethodMatchersWhenDefaultServletThenPermits() throws Exception {
this.spring.register(DefaultServletConfig.class)
.postProcessor((context) -> context.setServletContext(MockServletContext.mvc()))
.autowire();
this.mvc.perform(get("/path/method").with(defaultServlet())).andExpect(status().isNotFound());
this.mvc.perform(head("/path/method").with(defaultServlet())).andExpect(status().isUnauthorized());
this.mvc.perform(get("/path/method").with(servletPath("/path"))).andExpect(status().isUnauthorized());
}
@Test
void defaultServletWhenNoDefaultServletThenWiringException() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(DefaultServletConfig.class)
.postProcessor((context) -> context.setServletContext(new MockServletContext()))
.autowire());
}
@Test
void servletPathMatchersWhenMatchingServletThenPermits() throws Exception {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
this.spring.register(ServletPathConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/path/path").with(servletPath("/path"))).andExpect(status().isNotFound());
this.mvc.perform(get("/path/path").with(defaultServlet())).andExpect(status().isUnauthorized());
}
@Test
void servletPathHttpMethodMatchersWhenMatchingServletThenPermits() throws Exception {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
this.spring.register(ServletPathConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/path/method").with(servletPath("/path"))).andExpect(status().isNotFound());
this.mvc.perform(head("/path/method").with(servletPath("/path"))).andExpect(status().isUnauthorized());
this.mvc.perform(get("/path/method").with(defaultServlet())).andExpect(status().isUnauthorized());
}
@Test
void servletPathWhenNoMatchingPathThenWiringException() {
MockServletContext servletContext = MockServletContext.mvc();
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(ServletPathConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire());
}
@Test
void servletMappingMatchersWhenMatchingServletThenPermits() throws Exception {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("jsp", Servlet.class).addMapping("*.jsp");
this.spring.register(ServletMappingConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/path/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isNotFound());
this.mvc.perform(get("/path/file.jsp").with(defaultServlet())).andExpect(status().isUnauthorized());
}
@Test
void servletMappingHttpMethodMatchersWhenMatchingServletThenPermits() throws Exception {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("jsp", Servlet.class).addMapping("*.jsp");
this.spring.register(ServletMappingConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/method/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isNotFound());
this.mvc.perform(head("/method/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isUnauthorized());
this.mvc.perform(get("/method/file.jsp").with(defaultServlet())).andExpect(status().isUnauthorized());
}
@Test
void servletMappingWhenNoMatchingExtensionThenWiringException() {
MockServletContext servletContext = MockServletContext.mvc();
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(ServletMappingConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire());
}
@Test
void anyRequestWhenUsedWithDefaultServletThenDoesNotWire() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(MixedServletEndpointConfig.class).autowire())
.withMessageContaining("forServletPattern");
}
@Test
void servletWhenNoMatchingPathThenDenies() throws Exception {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
this.spring.register(DefaultServletAndServletPathConfig.class)
.postProcessor((context) -> context.setServletContext(servletContext))
.autowire();
this.mvc.perform(get("/js/color.js").with(servletPath("/js"))).andExpect(status().isUnauthorized());
this.mvc.perform(get("/mvc/controller").with(defaultServlet())).andExpect(status().isUnauthorized());
this.mvc.perform(get("/js/color.js").with(defaultServlet())).andExpect(status().isNotFound());
this.mvc.perform(get("/mvc/controller").with(servletPath("/mvc"))).andExpect(status().isUnauthorized());
this.mvc.perform(get("/mvc/controller").with(user("user")).with(servletPath("/mvc")))
.andExpect(status().isNotFound());
}
@Test
void permitAllWhenDefaultServletThenDoesNotWire() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(MixedServletPermitAllConfig.class).autowire())
.withMessageContaining("forServletPattern");
}
static RequestPostProcessor defaultServlet() {
return (request) -> {
String uri = request.getRequestURI();
request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping());
request.setServletPath(uri);
request.setPathInfo("");
return request;
};
}
static RequestPostProcessor servletPath(String path) {
return (request) -> {
String uri = request.getRequestURI();
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, path));
request.setServletPath(path);
request.setPathInfo(uri.substring(path.length()));
return request;
};
}
static RequestPostProcessor servletExtension(String extension) {
return (request) -> {
String uri = request.getRequestURI();
request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, extension));
request.setServletPath(uri);
request.setPathInfo("");
return request;
};
}
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
static class GrantedAuthorityDefaultHasRoleConfig { static class GrantedAuthorityDefaultHasRoleConfig {
@ -693,6 +892,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableWebMvc
static class AfterAnyRequestConfig { static class AfterAnyRequestConfig {
@Bean @Bean
@ -954,7 +1154,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
@Configuration @Configuration
@EnableWebMvc @EnableWebMvc
@EnableWebSecurity @EnableWebSecurity
static class ServletPathConfig { static class MvcServletPathConfig {
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
@ -1136,6 +1336,163 @@ public class AuthorizeHttpRequestsConfigurerTests {
} }
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class AuthorizeHttpRequestsConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/path/**").permitAll()
.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class DefaultServletConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.forServletPattern("/", (root) -> root
.requestMatchers(HttpMethod.GET, "/path/method/**").permitAll()
.requestMatchers("/path/path/**").permitAll()
.anyRequest().authenticated()
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class ServletPathConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.forServletPattern("/path/*", (root) -> root
.requestMatchers(HttpMethod.GET, "/method/**").permitAll()
.requestMatchers("/path/**").permitAll()
.anyRequest().authenticated()
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class ServletMappingConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.forServletPattern("*.jsp", (jsp) -> jsp
.requestMatchers(HttpMethod.GET, "/method/**").permitAll()
.requestMatchers("/path/**").permitAll()
.anyRequest().authenticated()
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class MixedServletEndpointConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.forServletPattern("/", (root) -> root.anyRequest().permitAll())
.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class MixedServletPermitAllConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.formLogin((form) -> form.loginPage("/page").permitAll())
.authorizeHttpRequests((requests) -> requests
.forServletPattern("/", (root) -> root
.anyRequest().authenticated()
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class DefaultServletAndServletPathConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.forServletPattern("/", (root) -> root
.requestMatchers("/js/**", "/css/**").permitAll()
)
.forServletPattern("/mvc/*", (mvc) -> mvc
.requestMatchers("/controller/**").authenticated()
)
.forServletPattern("*.jsp", (jsp) -> jsp
.anyRequest().authenticated()
)
);
// @formatter:on
return http.build();
}
}
@Configuration @Configuration
static class AuthorizationEventPublisherConfig { static class AuthorizationEventPublisherConfig {

View File

@ -0,0 +1,198 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.configurers.DispatcherServletDelegatingRequestMatcherBuilder.DispatcherServletDelegatingRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
public class RequestMatcherBuildersTests {
@Test
void matchersWhenDefaultDispatcherServletThenMvc() {
MockServletContext servletContext = MockServletContext.mvc();
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
List<RequestMatcher> matchers = builder.matchers("/mvc");
assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0);
assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull();
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc");
}
@Test
void httpMethodMatchersWhenDefaultDispatcherServletThenMvc() {
MockServletContext servletContext = MockServletContext.mvc();
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
List<RequestMatcher> matchers = builder.matchers(HttpMethod.GET, "/mvc");
assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0);
assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull();
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc");
assertThat(ReflectionTestUtils.getField(matcher, "method")).isEqualTo(HttpMethod.GET);
}
@Test
void matchersWhenPathDispatcherServletThenMvc() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
List<RequestMatcher> matchers = builder.matchers("/controller");
assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0);
assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isEqualTo("/mvc");
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller");
}
@Test
void matchersWhenAlsoExtraServletContainerMappingsThenRequiresServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path"))
.withMessageContaining(".forServletPattern");
}
@Test
void matchersWhenOnlyDefaultServletThenAnt() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
List<RequestMatcher> matchers = builder.matchers("/controller");
assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class);
AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0);
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller");
}
@Test
void matchersWhenNoHandlerMappingIntrospectorThenAnt() {
MockServletContext servletContext = MockServletContext.mvc();
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext, (context) -> {
});
List<RequestMatcher> matchers = builder.matchers("/controller");
assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class);
AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0);
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller");
}
@Test
void matchersWhenNoDispatchServletThenAnt() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
List<RequestMatcher> matchers = builder.matchers("/services/endpoint");
assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class);
AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0);
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/services/endpoint");
}
@Test
void matchersWhenMixedServletsThenServletPathDelegating() {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
assertThat(builder.matchers("/services/endpoint").get(0))
.isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
}
@Test
void matchersWhenDispatcherServletNotDefaultAndOtherServletsThenRequiresServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
.withMessageContaining(".forServletPattern");
}
@Test
void httpMatchersWhenDispatcherServletNotDefaultAndOtherServletsThenRequiresServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", Servlet.class).addMapping("/");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/pattern"))
.withMessageContaining(".forServletPattern");
}
@Test
void matchersWhenTwoDispatcherServletsThenException() {
MockServletContext servletContext = MockServletContext.mvc();
servletContext.addServlet("two", DispatcherServlet.class).addMapping("/other/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
.withMessageContaining(".forServletPattern");
}
@Test
void matchersWhenMoreThanOneMappingThenException() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
.withMessageContaining(".forServletPattern");
}
@Test
void matchersWhenMoreThanOneMappingAndDefaultServletsThenRequiresServletPath() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
.withMessageContaining(".forServletPattern");
}
RequestMatcherBuilder requestMatchersBuilder(ServletContext servletContext) {
return requestMatchersBuilder(servletContext, (context) -> {
context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class,
() -> mock(HandlerMappingIntrospector.class));
context.registerBean(ObjectPostProcessor.class, () -> mock(ObjectPostProcessor.class));
});
}
RequestMatcherBuilder requestMatchersBuilder(ServletContext servletContext,
Consumer<GenericWebApplicationContext> consumer) {
GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext);
consumer.accept(context);
context.refresh();
return RequestMatcherBuilders.createDefault(context);
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ServletPatternRequestMatcher}
*/
class ServletPatternRequestMatcherTests {
ServletPatternRequestMatcher matcher = new ServletPatternRequestMatcher("*.jsp");
@Test
void matchesWhenDefaultServletThenTrue() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, ".jsp"));
assertThat(this.matcher.matches(request)).isTrue();
}
@Test
void matchesWhenNotDefaultServletThenFalse() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/a"));
request.setServletPath("/a/uri.jsp");
assertThat(this.matcher.matches(request)).isFalse();
}
@Test
void matcherWhenDefaultServletThenTrue() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, ".jsp"));
request.setServletPath("/a/uri.jsp");
assertThat(this.matcher.matcher(request).isMatch()).isTrue();
}
@Test
void matcherWhenNotDefaultServletThenFalse() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/a"));
request.setServletPath("/a/uri.jsp");
assertThat(this.matcher.matcher(request).isMatch()).isFalse();
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2023 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.config.annotation.web.configurers;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.MappingMatch;
import org.springframework.mock.web.MockHttpServletMapping;
final class TestMockHttpServletMappings {
private TestMockHttpServletMappings() {
}
static MockHttpServletMapping extension(HttpServletRequest request, String extension) {
String uri = request.getRequestURI();
String matchValue = uri.substring(0, uri.lastIndexOf(extension));
return new MockHttpServletMapping(matchValue, "*" + extension, "extension", MappingMatch.EXTENSION);
}
static MockHttpServletMapping path(HttpServletRequest request, String path) {
String uri = request.getRequestURI();
String matchValue = uri.substring(path.length());
return new MockHttpServletMapping(matchValue, path + "/*", "path", MappingMatch.PATH);
}
static MockHttpServletMapping defaultMapping() {
return new MockHttpServletMapping("", "/", "default", MappingMatch.DEFAULT);
}
}

View File

@ -571,70 +571,156 @@ http {
---- ----
==== ====
[[match-by-servlet-path]]
[[mvc-not-default-servlet]]
[[match-by-mvc]] [[match-by-mvc]]
=== Using an MvcRequestMatcher === Matching by Servlet Pattern
Generally speaking, you can use `requestMatchers(String)` as demonstrated above. Generally speaking, you can use `requestMatchers(String...)` and `requestMatchers(HttpMethod, String...)` as demonstrated above.
However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration. However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration.
For example, if Spring MVC is mapped to `/spring-mvc` instead of `/` (the default), then you may have an endpoint like `/spring-mvc/my/controller` that you want to authorize. For example, if Spring MVC is mapped to `/mvc` instead of `/` (the default), then you may have an endpoint like `/mvc/my/controller` that you want to authorize.
You need to use `MvcRequestMatcher` to split the servlet path and the controller path in your configuration like so: If you have multiple servlets, and `DispatcherServlet` is mapped in this way, you'll see an error that's something like this:
[source,bash]
----
This method cannot decide whether these patterns are Spring MVC patterns or not
...
For your reference, here is your servlet configuration: {default=[/], dispatcherServlet=[/mvc/*]}
To address this, you need to specify the servlet path or pattern for each endpoint.
You can use .forServletPattern in conjunction with requestMatchers do to this
----
You can use `.forServletPattern` (or construct your own `MvcRequestMatcher` instance) to split the servlet path and the controller path in your configuration, like so:
.Match by MvcRequestMatcher .Match by MvcRequestMatcher
==== ====
.Java .Java
[source,java,role="primary"] [source,java,role="primary"]
---- ----
@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
}
@Bean @Bean
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) { SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
http http
.authorizeHttpRequests((authorize) -> authorize .authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller") .forServletPattern("/mvc/*", (mvc) -> mvc
.requestMatchers("/my/resource/**").hasAuthority("resource:read")
.anyRequest().authenticated() .anyRequest().authenticated()
)
); );
return http.build(); return http.build();
} }
---- ----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
@Bean
fun appEndpoints(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
http {
authorizeHttpRequests {
authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
authorize(anyRequest, authenticated)
}
}
----
.Xml
[source,xml,role="secondary"]
----
<http>
<intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
----
==== ====
where `/mvc/*` is the matching pattern in your servlet configuration listed in the error message.
This need can arise in at least two different ways: This need can arise in at least two different ways:
* If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else * If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else
* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default path) * If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default servlet)
Note that when either of these cases come up, all URIs need to be fully-qualified as above.
For example, consider a more sophisticated setup where you have Spring MVC resources mapped to `/mvc/*` and Spring Boot H2 Console mapped to `/h2-console/*`.
In that case, each URI can be made absolute, listing the servlet path like so:
.Match by Servlet Path
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.forServletPattern("/mvc/*", (mvc) -> mvc
.requestMatchers("/my/resource/**").hasAuthority("resource:read")
)
.forServletPattern("/h2-console/*", (h2) -> h2
.anyRequest().hasAuthority("h2")
)
)
// ...
}
----
====
Alternatively, you can do one of three things to remove the need to disambiguate:
1. Always deploy `DispatcherServlet` to `/` (the default behavior)
+
When `DispatcherServlet` is mapped to `/`, it's clear that all the URIs supplied in `requestMatchers(String)` are absolute URIs.
Because of that, there is no ambiguity when interpreting them.
+
2. Remove all other servlets
+
When there is only `DispatcherServlet`, it's clear that all the URIs supplied in `requestMatchers(String)` are relative to the Spring MVC configuration.
Because of that, there is no ambiguity when interpreting them.
At times, servlet containers add other servlets by default that you aren't actually using.
So, if these aren't needed, remove them, bringing you down to just `DispatcherServlet`.
+
3. Create an `HttpRequestHandler` so that `DispatcherServlet` dispatches to your servlets instead of your servlet container.
+
If you are deploying Spring MVC to a separate path to allow your container to serve static resources, consider instead {spring-framework-reference-url}web/webmvc/mvc-config/default-servlet-handler.html#page-title[notifying Spring MVC about this].
Or, if you have a custom servlet, publishing {spring-framework-api-url}org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.html[a custom `HttpRequestHandler` bean within {spring-framework-api-url}org/springframework/web/servlet/DispatcherServlet.html[the `DispatcherServlet` configuration] instead.
+
=== Matching by the Default Servlet
You can also match more generally by the matching pattern specified in your servlet configuration.
For example, to match the default servlet (whichever servlet is mapped to `/`), use `forServletPattern` like so:
.Match by the Default Servlet
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.forServletPattern("/", (root) -> root
.requestMatchers("/my/resource/**").hasAuthority("resource:read")
)
)
// ...
}
----
====
Such will match on requests that the servlet container matches to your default servlet that start with the URI `/my/resource`.
=== Matching by an Extension Servlet
Or, to match to an extension servlet (like a servlet mapped to `*.jsp`), use `forServletPattern` as follows:
.Match by an Extension Servlet
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.forServletPattern("*.jsp", (jsp) -> jsp
.requestMatchers("/my/resource/**").hasAuthority("resource:read")
)
)
// ...
}
----
====
Such will match on requests that the servlet container matches to your `*.jsp` servlet that start with the URI `/my/resource` (for example a request like `/my/resource/page.jsp`).
[[match-by-custom]] [[match-by-custom]]
=== Using a Custom Matcher === Using a Custom Matcher