parent
7e9d707c7d
commit
762319b6be
|
@ -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, "/**");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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><default> + <strong>`/js/**`</strong> ==> `/js/**`</li>
|
||||||
|
* <li><default> + <strong>`/css/**`</strong> ==> `/css/**`</li>
|
||||||
|
* <li>`/mvc` + <strong>`/my/controller/**`</strong> ==>
|
||||||
|
* `/mvc/my/controller/**`</li>
|
||||||
|
* <li>`/h2-console` + <strong><any request></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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
.anyRequest().authenticated()
|
.requestMatchers("/my/resource/**").hasAuthority("resource:read")
|
||||||
|
.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
|
||||||
|
|
Loading…
Reference in New Issue