17 changed files with 2190 additions and 76 deletions
@ -0,0 +1,52 @@
@@ -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 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,103 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,349 @@
@@ -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"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,198 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue