1
0
mirror of synced 2026-07-10 05:10:03 +00:00

Compare commits

...

31 Commits

Author SHA1 Message Date
github-actions[bot] d02ab50577 Release 5.8.5 2023-07-17 21:04:33 +00:00
Josh Cummings bb46a54270 Add DispatcherServlet to Tests
Issue gh-13551
2023-07-17 10:58:30 -06:00
Josh Cummings df239b6448 Improve RequestMatcher Validation
Closes gh-13551
2023-07-17 08:41:30 -06:00
Marcus Da Coregio a939f17890 Merge branch '5.7.x' into 5.8.x 2023-07-17 09:15:56 -03:00
Marcus Da Coregio fe9bc26bdc Merge branch '5.6.x' into 5.7.x 2023-07-17 09:13:28 -03:00
Marcus Da Coregio 7813a9ba26 Use default PathPatternParser instance 2023-07-17 09:12:28 -03:00
Marcus Da Coregio 7d58b9391a Merge branch '5.7.x' into 5.8.x 2023-07-14 11:04:26 -03:00
Marcus Da Coregio dc3cb68c76 Merge branch '5.6.x' into 5.7.x
Closes gh-dry-run
2023-07-14 11:04:01 -03:00
Marcus Da Coregio 8555c100d7 Update org.springframework.data to 2021.2.14
Closes gh-13516
2023-07-14 10:53:12 -03:00
Marcus Da Coregio ae46531add Update org.springframework to 5.3.29
Closes gh-13515
2023-07-14 10:53:08 -03:00
Marcus Da Coregio 0121b08a85 Update io.projectreactor to 2020.0.34
Closes gh-13513
2023-07-14 10:53:01 -03:00
Marcus Da Coregio 56292c9971 Update org.springframework.data to 2021.2.14
Closes gh-13512
2023-07-14 10:52:14 -03:00
Marcus Da Coregio 4fc938555e Update org.springframework to 5.3.29
Closes gh-13511
2023-07-14 10:52:11 -03:00
Marcus Da Coregio e574415244 Update io.projectreactor to 2020.0.34
Closes gh-13509
2023-07-14 10:52:03 -03:00
Marcus Da Coregio c5da886310 Update org.springframework to 5.3.29
Closes gh-13508
2023-07-14 10:42:27 -03:00
Marcus Da Coregio 3bef5fd3ed Update io.projectreactor to 2020.0.34
Closes gh-13505
2023-07-14 10:42:27 -03:00
Steve Riesenberg b7a9a654f0 Merge branch '5.7.x' into 5.8.x 2023-07-12 16:06:02 -05:00
Steve Riesenberg e7201c48d1 Fix copy/paste from main
Issue gh-13500
2023-07-12 16:04:35 -05:00
Steve Riesenberg a642fdb004 Merge branch '5.7.x' into 5.8.x 2023-07-12 15:52:55 -05:00
Steve Riesenberg 991b398c55 Create release with full docs path
Closes gh-13500
2023-07-12 15:51:44 -05:00
Josh Cummings 40d61743b9 Replace Existing Continue Parameter
Closes gh-13438
2023-07-10 16:12:05 -06:00
Josh Cummings 8895a66a2b Add hasIpAddress Migration Steps
Closes gh-13474
2023-07-10 13:35:16 -06:00
Marcus Da Coregio 80a5028f3f saml2Login filterProcessingUrl should be loginProcessingUrl
Closes gh-13417
2023-06-23 10:38:04 -03:00
Marcus Da Coregio c30bacac10 Improve Security Filters Documentation
Closes gh-8167
2023-06-22 10:11:18 -03:00
Marcus Da Coregio a104dec30d Merge branch '5.7.x' into 5.8.x 2023-06-19 14:18:50 -03:00
Marcus Da Coregio 488b6ea531 Merge branch '5.6.x' into 5.7.x 2023-06-19 14:18:23 -03:00
github-actions[bot] ef04ea9d68 Next development version 2023-06-19 17:05:13 +00:00
github-actions[bot] b7be4c462c Next development version 2023-06-19 16:55:18 +00:00
Marcus Da Coregio aa8440e1ee Release 5.6.11 2023-06-19 13:23:20 -03:00
github-actions[bot] 6d3f1699c0 Release 5.7.9 2023-06-19 16:09:53 +00:00
github-actions[bot] bb72eed8d1 Next development version 2023-06-19 16:02:54 +00:00
21 changed files with 721 additions and 85 deletions
@@ -16,6 +16,10 @@
package org.springframework.gradle.sagan;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.Assert;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
@@ -25,6 +29,8 @@ import org.springframework.gradle.github.user.User;
public class SaganCreateReleaseTask extends DefaultTask {
private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.+)?$");
@Input
private String gitHubAccessToken;
@Input
@@ -44,9 +50,12 @@ public class SaganCreateReleaseTask extends DefaultTask {
// Antora reference docs URLs for snapshots do not contain -SNAPSHOT
String referenceDocUrl = this.referenceDocUrl;
if (this.version.endsWith("-SNAPSHOT")) {
referenceDocUrl = this.referenceDocUrl
.replace("{version}", this.version)
.replace("-SNAPSHOT", "");
Matcher versionMatcher = VERSION_PATTERN.matcher(this.version);
Assert.isTrue(versionMatcher.matches(), "Version " + this.version + " does not match expected pattern");
String majorVersion = versionMatcher.group(1);
String minorVersion = versionMatcher.group(2);
String majorMinorVersion = String.format("%s.%s-SNAPSHOT", majorVersion, minorVersion);
referenceDocUrl = this.referenceDocUrl.replace("{version}", majorMinorVersion);
}
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
@@ -19,8 +19,11 @@ package org.springframework.security.config.annotation.web;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
@@ -36,6 +39,7 @@ import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
/**
@@ -297,14 +301,47 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @since 5.8
*/
public C requestMatchers(HttpMethod method, String... patterns) {
List<RequestMatcher> matchers = new ArrayList<>();
if (mvcPresent) {
matchers.addAll(createMvcMatchers(method, patterns));
if (!mvcPresent) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
else {
matchers.addAll(RequestMatchers.antMatchers(method, patterns));
if (!(this.context instanceof WebApplicationContext)) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
WebApplicationContext context = (WebApplicationContext) this.context;
ServletContext servletContext = context.getServletContext();
if (servletContext == null) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
Map<String, ? extends ServletRegistration> registrations = servletContext.getServletRegistrations();
if (registrations == null) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
if (!hasDispatcherServlet(registrations)) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
Assert.isTrue(registrations.size() == 1,
"This method cannot decide whether these patterns are Spring MVC patterns or not. If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).");
return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0]));
}
private boolean hasDispatcherServlet(Map<String, ? extends ServletRegistration> registrations) {
if (registrations == null) {
return false;
}
Class<?> dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet",
null);
for (ServletRegistration registration : registrations.values()) {
try {
Class<?> clazz = Class.forName(registration.getClassName());
if (dispatcherServlet.isAssignableFrom(clazz)) {
return true;
}
}
catch (ClassNotFoundException ex) {
return false;
}
}
return false;
}
/**
@@ -380,12 +417,7 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @return a {@link List} of {@link AntPathRequestMatcher} instances
*/
static List<RequestMatcher> antMatchers(HttpMethod httpMethod, String... antPatterns) {
String method = (httpMethod != null) ? httpMethod.toString() : null;
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : antPatterns) {
matchers.add(new AntPathRequestMatcher(pattern, method));
}
return matchers;
return Arrays.asList(antMatchersAsArray(httpMethod, antPatterns));
}
/**
@@ -399,6 +431,15 @@ public abstract class AbstractRequestMatcherRegistry<C> {
return antMatchers(null, antPatterns);
}
static RequestMatcher[] antMatchersAsArray(HttpMethod httpMethod, String... antPatterns) {
String method = (httpMethod != null) ? httpMethod.toString() : null;
RequestMatcher[] matchers = new RequestMatcher[antPatterns.length];
for (int index = 0; index < antPatterns.length; index++) {
matchers[index] = new AntPathRequestMatcher(antPatterns[index], method);
}
return matchers;
}
/**
* Create a {@link List} of {@link RegexRequestMatcher} instances.
* @param httpMethod the {@link HttpMethod} to use or {@code null} for any
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -16,6 +16,7 @@
package org.springframework.security.config.web.server;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpMethod;
@@ -23,6 +24,8 @@ import org.springframework.security.web.server.util.matcher.OrServerWebExchangeM
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* @author Rob Winch
@@ -62,7 +65,8 @@ public abstract class AbstractServerWebExchangeMatcherRegistry<T> {
* {@link ServerWebExchangeMatcher}
*/
public T pathMatchers(HttpMethod method, String... antPatterns) {
return matcher(ServerWebExchangeMatchers.pathMatchers(method, antPatterns));
List<PathPattern> pathPatterns = parsePatterns(antPatterns);
return matcher(ServerWebExchangeMatchers.pathMatchers(method, pathPatterns.toArray(new PathPattern[0])));
}
/**
@@ -74,7 +78,19 @@ public abstract class AbstractServerWebExchangeMatcherRegistry<T> {
* {@link ServerWebExchangeMatcher}
*/
public T pathMatchers(String... antPatterns) {
return matcher(ServerWebExchangeMatchers.pathMatchers(antPatterns));
List<PathPattern> pathPatterns = parsePatterns(antPatterns);
return matcher(ServerWebExchangeMatchers.pathMatchers(pathPatterns.toArray(new PathPattern[0])));
}
private List<PathPattern> parsePatterns(String[] antPatterns) {
PathPatternParser parser = getPathPatternParser();
List<PathPattern> pathPatterns = new ArrayList<>(antPatterns.length);
for (String pattern : antPatterns) {
pattern = parser.initFullPathPattern(pattern);
PathPattern pathPattern = parser.parse(pattern);
pathPatterns.add(pathPattern);
}
return pathPatterns;
}
/**
@@ -96,6 +112,10 @@ public abstract class AbstractServerWebExchangeMatcherRegistry<T> {
*/
protected abstract T registerMatcher(ServerWebExchangeMatcher matcher);
protected PathPatternParser getPathPatternParser() {
return PathPatternParser.defaultInstance;
}
/**
* Associates a {@link ServerWebExchangeMatcher} instances
* @param matcher the {@link ServerWebExchangeMatcher} instance
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@@ -190,9 +190,11 @@ import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsProcessor;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but
@@ -1562,6 +1564,18 @@ public class ServerHttpSecurity {
return null;
}
private <T> T getBeanOrNull(String beanName, Class<T> requiredClass) {
if (this.context == null) {
return null;
}
try {
return this.context.getBean(beanName, requiredClass);
}
catch (Exception ex) {
return null;
}
}
private <T> String[] getBeanNamesForTypeOrEmpty(Class<T> beanClass) {
if (this.context == null) {
return new String[0];
@@ -1582,6 +1596,8 @@ public class ServerHttpSecurity {
*/
public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegistry<AuthorizeExchangeSpec.Access> {
private static final String REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME = "requestMappingHandlerMapping";
private DelegatingReactiveAuthorizationManager.Builder managerBldr = DelegatingReactiveAuthorizationManager
.builder();
@@ -1589,6 +1605,8 @@ public class ServerHttpSecurity {
private boolean anyExchangeRegistered;
private PathPatternParser pathPatternParser;
/**
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
* @return the {@link ServerHttpSecurity} to continue configuring
@@ -1608,6 +1626,22 @@ public class ServerHttpSecurity {
return result;
}
@Override
protected PathPatternParser getPathPatternParser() {
if (this.pathPatternParser != null) {
return this.pathPatternParser;
}
RequestMappingHandlerMapping requestMappingHandlerMapping = getBeanOrNull(
REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME, RequestMappingHandlerMapping.class);
if (requestMappingHandlerMapping != null) {
this.pathPatternParser = requestMappingHandlerMapping.getPathPatternParser();
}
if (this.pathPatternParser == null) {
this.pathPatternParser = PathPatternParser.defaultInstance;
}
return this.pathPatternParser;
}
@Override
protected Access registerMatcher(ServerWebExchangeMatcher matcher) {
Assert.state(!this.anyExchangeRegistered, () -> "Cannot register " + matcher
@@ -0,0 +1,139 @@
/*
* Copyright 2002-2022 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;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.ServletRegistration;
import javax.servlet.ServletSecurityElement;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.DispatcherServlet;
public class MockServletContext extends org.springframework.mock.web.MockServletContext {
private final Map<String, ServletRegistration> registrations = new LinkedHashMap<>();
public static MockServletContext mvc() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class);
return servletContext;
}
@NonNull
@Override
public ServletRegistration.Dynamic addServlet(@NonNull String servletName, Class<? extends Servlet> clazz) {
ServletRegistration.Dynamic dynamic = new MockServletRegistration(servletName, clazz);
this.registrations.put(servletName, dynamic);
return dynamic;
}
@NonNull
@Override
public Map<String, ? extends ServletRegistration> getServletRegistrations() {
return this.registrations;
}
private static class MockServletRegistration implements ServletRegistration.Dynamic {
private final String name;
private final Class<?> clazz;
MockServletRegistration(String name, Class<?> clazz) {
this.name = name;
this.clazz = clazz;
}
@Override
public void setLoadOnStartup(int loadOnStartup) {
}
@Override
public Set<String> setServletSecurity(ServletSecurityElement constraint) {
return null;
}
@Override
public void setMultipartConfig(MultipartConfigElement multipartConfig) {
}
@Override
public void setRunAsRole(String roleName) {
}
@Override
public void setAsyncSupported(boolean isAsyncSupported) {
}
@Override
public Set<String> addMapping(String... urlPatterns) {
return null;
}
@Override
public Collection<String> getMappings() {
return null;
}
@Override
public String getRunAsRole() {
return null;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getClassName() {
return this.clazz.getName();
}
@Override
public boolean setInitParameter(String name, String value) {
return false;
}
@Override
public String getInitParameter(String name) {
return null;
}
@Override
public Set<String> setInitParameters(Map<String, String> initParameters) {
return null;
}
@Override
public Map<String, String> getInitParameters() {
return null;
}
}
}
@@ -21,6 +21,7 @@ import java.lang.reflect.Modifier;
import java.util.List;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -28,12 +29,15 @@ 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.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -56,12 +60,15 @@ public class AbstractRequestMatcherRegistryTests {
private TestRequestMatcherRegistry matcherRegistry;
private WebApplicationContext context;
@BeforeEach
public void setUp() {
this.matcherRegistry = new TestRequestMatcherRegistry();
ApplicationContext context = mock(ApplicationContext.class);
given(context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR);
this.matcherRegistry.setApplicationContext(context);
this.context = mock(WebApplicationContext.class);
given(this.context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR);
given(this.context.getServletContext()).willReturn(MockServletContext.mvc());
this.matcherRegistry.setApplicationContext(this.context);
}
@Test
@@ -184,6 +191,32 @@ public class AbstractRequestMatcherRegistryTests {
"Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext");
}
@Test
public void requestMatchersWhenNoDispatcherServletThenAntPathRequestMatcherType() {
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
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
public void requestMatchersWhenAmbiguousServletsThenException() {
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class);
servletContext.addServlet("servletTwo", Servlet.class);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
}
private void mockMvcIntrospector(boolean isPresent) {
ApplicationContext context = this.matcherRegistry.getApplicationContext();
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
@@ -29,10 +29,10 @@ import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -75,7 +75,7 @@ public class AuthorizeRequestsTests {
@BeforeEach
public void setup() {
this.servletContext = spy(new MockServletContext());
this.servletContext = spy(MockServletContext.mvc());
this.request = new MockHttpServletRequest("GET", "");
this.request.setMethod("GET");
this.response = new MockHttpServletResponse();
@@ -34,7 +34,7 @@ import org.springframework.core.annotation.Order;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -233,7 +233,7 @@ public class HttpSecuritySecurityMatchersTests {
public void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.setServletContext(new MockServletContext());
this.context.setServletContext(MockServletContext.mvc());
this.context.refresh();
this.context.getAutowireCapableBeanFactory().autowireBean(this);
}
@@ -31,8 +31,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -167,7 +167,7 @@ public class UrlAuthorizationConfigurerTests {
public void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.setServletContext(new MockServletContext());
this.context.setServletContext(MockServletContext.mvc());
this.context.refresh();
this.context.getAutowireCapableBeanFactory().autowireBean(this);
}
@@ -28,8 +28,8 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.util.InMemoryXmlWebApplicationContext;
import org.springframework.test.context.web.GenericXmlWebContextLoader;
import org.springframework.test.web.servlet.MockMvc;
@@ -129,7 +129,7 @@ public class SpringTestContext implements Closeable {
public ConfigurableWebApplicationContext getContext() {
if (!this.context.isRunning()) {
this.context.setServletContext(new MockServletContext());
this.context.setServletContext(MockServletContext.mvc());
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
@@ -137,7 +137,7 @@ public class SpringTestContext implements Closeable {
}
public void autowire() {
this.context.setServletContext(new MockServletContext());
this.context.setServletContext(MockServletContext.mvc());
this.context.setServletConfig(new MockServletConfig());
for (Consumer<ConfigurableWebApplicationContext> postProcessor : this.postProcessors) {
postProcessor.accept(this.context);
+2 -2
View File
@@ -8,11 +8,11 @@ javaPlatform {
dependencies {
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
api platform("io.projectreactor:reactor-bom:2020.0.33")
api platform("io.projectreactor:reactor-bom:2020.0.34")
api platform("io.rsocket:rsocket-bom:1.1.4")
api platform("org.junit:junit-bom:5.9.3")
api platform("org.mockito:mockito-bom:4.8.1")
api platform("org.springframework.data:spring-data-bom:2021.2.13")
api platform("org.springframework.data:spring-data-bom:2021.2.14")
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
@@ -1090,6 +1090,59 @@ Xml::
----
======
=== Migrate `hasIpAddress` to `access(AuthorizationManager)`
`hasIpAddress` has no DSL equivalent in `authorizeHttpRequests`.
As such, you need to change any called to `hasIpAddress` to using an `AuthorizationManager`.
First, construct an `IpAddressMatcher` like so:
====
.Java
[source,java,role="primary"]
----
IpAddressMatcher hasIpAddress = new IpAddressMatcher("127.0.0.1");
----
====
And then change from this:
====
.Java
[source,java,role="primary"]
----
http
.authorizeRequests((authorize) -> authorize
.mvcMatchers("/app/**").hasIpAddress("127.0.0.1")
// ...
.anyRequest().denyAll()
)
// ...
----
====
to this:
====
.Java
[source,java,role="primary"]
----
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/app/**").access((authentication, context) ->
new AuthorizationDecision(hasIpAddress.matches(context.getRequest()))
// ...
.anyRequest().denyAll()
)
// ...
----
====
[NOTE]
Securing by IP Address is quite fragile to begin with.
For that reason, there are no plans to port this support over to `authorizeHttpRequests`.
=== Migrate SpEL expressions to `AuthorizationManager`
For authorization rules, Java tends to be easier to test and maintain than SpEL.
+263 -36
View File
@@ -164,46 +164,224 @@ In fact, a `SecurityFilterChain` might have zero security ``Filter``s if the app
[[servlet-security-filters]]
== Security Filters
Spring Security uses a number of Servlet Filters (https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.pdf[Jakarta Servlet Spec, Chapter 6]) to provide security to your application.
The Security Filters are inserted into the <<servlet-filterchainproxy>> with the <<servlet-securityfilterchain>> API.
The <<servlet-filters-review,order of ``Filter``>>s matters.
Those filters can be used for a number of different purposes, like xref:servlet/authentication/index.adoc[authentication], xref:servlet/authorization/index.adoc[authorization], xref:servlet/exploits/index.adoc[exploit protection], and more.
The filters are executed in a specific order to guarantee that they are invoked at the right time, for example, the `Filter` that performs authentication should be invoked before the `Filter` that performs authorization.
It is typically not necessary to know the ordering of Spring Security's ``Filter``s.
However, there are times that it is beneficial to know the ordering
However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the {gh-url}/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java[`FilterOrderRegistration` code].
Below is a comprehensive list of Spring Security Filter ordering:
To exemplify the above paragraph, let's consider the following security configuration:
====
.Java
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic { }
formLogin { }
}
return http.build()
}
}
----
====
The above configuration will result in the following `Filter` ordering:
[cols="1,1", options="header"]
|====
| Filter | Added by
| xref:servlet/exploits/csrf.adoc[CsrfFilter] | `HttpSecurity#csrf`
| xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[UsernamePasswordAuthenticationFilter] | `HttpSecurity#formLogin`
| xref:servlet/authentication/passwords/basic.adoc[BasicAuthenticationFilter] | `HttpSecurity#httpBasic`
| xref:servlet/authorization/authorize-http-requests.adoc[AuthorizationFilter] | `HttpSecurity#authorizeHttpRequests`
|====
1. First, the `CsrfFilter` is invoked to protect against xref:servlet/exploits/csrf.adoc[CSRF attacks].
2. Second, the authentication filters are invoked to authenticate the request.
3. Third, the `AuthorizationFilter` is invoked to authorize the request.
[NOTE]
====
There might be other `Filter` instances that are not listed above.
If you want to see the list of filters invoked for a particular request, you can <<servlet-print-filters,print them>>.
====
[[servlet-print-filters]]
=== Printing the Security Filters
Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request.
For example, you want to make sure that the <<adding-custom-filter,filter you have added>> is in the list of the security filters.
The list of filters is printed at INFO level on the application startup, so you can see something like the following on the console output for example:
[source,text,role="terminal"]
----
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
----
And that will give a pretty good idea of the security filters that are configured for <<servlet-securityfilterchain,each filter chain>>.
But that is not all, you can also configure your application to print the invocation of each individual filter for each request.
That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from.
To do that, you can configure your application to <<servlet-logging,log the security events>>.
[[adding-custom-filter]]
=== Adding a Custom Filter to the Filter Chain
Mostly of the times, the default security filters are enough to provide security to your application.
However, there might be times that you want to add a custom `Filter` to the security filter chain.
For example, let's say that you want to add a `Filter` that gets a tenant id header and check if the current user has access to that tenant.
The previous description already gives us a clue on where to add the filter, since we need to know the current user, we need to add it after the authentication filters.
First, let's create the `Filter`:
[source,java]
----
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); <1>
boolean hasAccess = isUserAllowed(tenantId); <2>
if (hasAccess) {
filterChain.doFilter(request, response); <3>
return;
}
throw new AccessDeniedException("Access denied"); <4>
}
}
----
The sample code above does the following:
<1> Get the tenant id from the request header.
<2> Check if the current user has access to the tenant id.
<3> If the user has access, then invoke the rest of the filters in the chain.
<4> If the user does not have access, then throw an `AccessDeniedException`.
[TIP]
====
Instead of implementing `Filter`, you can extend from {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter] which is a base class for filters that are only invoked once per request and provides a `doFilterInternal` method with the `HttpServletRequest` and `HttpServletResponse` parameters.
====
Now, we need to add the filter to the security filter chain.
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class); <1>
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) <1>
return http.build()
}
----
====
<1> Use `HttpSecurity#addFilterBefore` to add the `TenantFilter` before the `AuthorizationFilter`.
By adding the filter before the `AuthorizationFilter` we are making sure that the `TenantFilter` is invoked after the authentication filters.
You can also use `HttpSecurity#addFilterAfter` to add the filter after a particular filter or `HttpSecurity#addFilterAt` to add the filter at a particular filter position in the filter chain.
And that's it, now the `TenantFilter` will be invoked in the filter chain and will check if the current user has access to the tenant id.
Be careful when you declare your filter as a Spring bean, either by annotating it with `@Component` or by declaring it as a bean in your configuration, because Spring Boot will automatically {spring-boot-reference-url}web.html#web.servlet.embedded-container.servlets-filters-listeners.beans[register it with the embedded container].
That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.
If you still want to declare your filter as a Spring bean to take advantage of dependency injection for example, and avoid the duplicate invocation, you can tell Spring Boot to not register it with the container by declaring a `FilterRegistrationBean` bean and setting its `enabled` property to `false`:
[source,java]
----
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
----
* xref:servlet/authentication/session-management.adoc#session-mgmt-force-session-creation[`ForceEagerSessionCreationFilter`]
* ChannelProcessingFilter
* WebAsyncManagerIntegrationFilter
* SecurityContextPersistenceFilter
* HeaderWriterFilter
* CorsFilter
* CsrfFilter
* LogoutFilter
* OAuth2AuthorizationRequestRedirectFilter
* Saml2WebSsoAuthenticationRequestFilter
* X509AuthenticationFilter
* AbstractPreAuthenticatedProcessingFilter
* CasAuthenticationFilter
* OAuth2LoginAuthenticationFilter
* Saml2WebSsoAuthenticationFilter
* xref:servlet/authentication/passwords/form.adoc#servlet-authentication-usernamepasswordauthenticationfilter[`UsernamePasswordAuthenticationFilter`]
* OpenIDAuthenticationFilter
* DefaultLoginPageGeneratingFilter
* DefaultLogoutPageGeneratingFilter
* ConcurrentSessionFilter
* xref:servlet/authentication/passwords/digest.adoc#servlet-authentication-digest[`DigestAuthenticationFilter`]
* BearerTokenAuthenticationFilter
* xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[`BasicAuthenticationFilter`]
* <<requestcacheawarefilter,RequestCacheAwareFilter>>
* SecurityContextHolderAwareRequestFilter
* JaasApiIntegrationFilter
* RememberMeAuthenticationFilter
* AnonymousAuthenticationFilter
* OAuth2AuthorizationCodeGrantFilter
* SessionManagementFilter
* <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>>
* xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]
* SwitchUserFilter
[[servlet-exceptiontranslationfilter]]
== Handling Security Exceptions
@@ -333,3 +511,52 @@ XML::
=== RequestCacheAwareFilter
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to save the `HttpServletRequest`.
[[servlet-logging]]
== Logging
Spring Security provides comprehensive logging of all security related events at the DEBUG and TRACE level.
This can be very useful when debugging your application because for security measures Spring Security does not add any detail of why a request has been rejected to the response body.
If you come across a 401 or 403 error, it is very likely that you will find a log message that will help you understand what is going on.
Let's consider an example where a user tries to make a `POST` request to a resource that has xref:servlet/exploits/csrf.adoc[CSRF protection] enabled without the CSRF token.
With no logs, the user will see a 403 error with no explanation of why the request was rejected.
However, if you enable logging for Spring Security, you will see a log message like this:
[source,text]
----
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
----
It becomes clear that the CSRF token is missing and that is why the request is being denied.
To configure your application to log all the security events, you can add the following to your application:
====
.application.properties in Spring Boot
[source,properties,role="primary"]
----
logging.level.org.springframework.security=TRACE
----
.logback.xml
[source,xml,role="secondary"]
----
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- ... -->
</appender>
<!-- ... -->
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
</configuration>
----
====
@@ -56,7 +56,7 @@ Java::
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
http
// ...
.saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
.saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
// ...
return http.build();
@@ -72,7 +72,7 @@ fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
http {
// ...
.saml2Login {
filterProcessingUrl = "/saml2/login/sso"
loginProcessingUrl = "/saml2/login/sso"
}
// ...
}
+4 -2
View File
@@ -40,7 +40,8 @@ def generateAttributes() {
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
def springBootReferenceUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/reference/html/"
return ['gh-old-samples-url': ghOldSamplesUrl.toString(),
'gh-samples-url': ghSamplesUrl.toString(),
'gh-url': ghUrl.toString(),
@@ -48,7 +49,8 @@ def generateAttributes() {
'security-reference-url': securityReferenceUrl.toString(),
'spring-framework-api-url': springFrameworkApiUrl.toString(),
'spring-framework-reference-url': springFrameworkReferenceUrl.toString(),
'spring-security-version': project.version]
'spring-boot-reference-url': springBootReferenceUrl.toString(),
'spring-security-version': project.version]
+ resolvedVersions(project.configurations.testRuntimeClasspath)
}
+3 -3
View File
@@ -1,9 +1,9 @@
aspectjVersion=1.9.19
springJavaformatVersion=0.0.39
springBootVersion=2.4.2
springFrameworkVersion=5.3.28
springBootVersion=2.7.12
springFrameworkVersion=5.3.29
openSamlVersion=3.4.6
version=5.8.4
version=5.8.5
kotlinVersion=1.7.22
samplesBranch=5.8.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
@@ -38,6 +38,7 @@ import org.springframework.security.web.PortResolver;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Represents central information from a {@code HttpServletRequest}.
@@ -372,10 +373,8 @@ public class DefaultSavedRequest implements SavedRequest {
if (queryString == null || queryString.length() == 0) {
return matchingRequestParameterName;
}
if (queryString.endsWith("&")) {
return queryString + matchingRequestParameterName;
}
return queryString + "&" + matchingRequestParameterName;
return UriComponentsBuilder.newInstance().query(queryString).replaceQueryParam(matchingRequestParameterName)
.queryParam(matchingRequestParameterName).build().getQuery();
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* 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.
@@ -42,8 +42,6 @@ public final class PathPatternParserServerWebExchangeMatcher implements ServerWe
private static final Log logger = LogFactory.getLog(PathPatternParserServerWebExchangeMatcher.class);
private static final PathPatternParser DEFAULT_PATTERN_PARSER = new PathPatternParser();
private final PathPattern pattern;
private final HttpMethod method;
@@ -60,7 +58,7 @@ public final class PathPatternParserServerWebExchangeMatcher implements ServerWe
public PathPatternParserServerWebExchangeMatcher(String pattern, HttpMethod method) {
Assert.notNull(pattern, "pattern cannot be null");
this.pattern = DEFAULT_PATTERN_PARSER.parse(pattern);
this.pattern = parse(pattern);
this.method = method;
}
@@ -68,6 +66,12 @@ public final class PathPatternParserServerWebExchangeMatcher implements ServerWe
this(pattern, null);
}
private PathPattern parse(String pattern) {
PathPatternParser parser = PathPatternParser.defaultInstance;
pattern = parser.initFullPathPattern(pattern);
return parser.parse(pattern);
}
@Override
public Mono<MatchResult> matches(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -23,6 +23,7 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
/**
* Provides factory methods for creating common {@link ServerWebExchangeMatcher}
@@ -59,6 +60,30 @@ public abstract class ServerWebExchangeMatchers {
return pathMatchers(null, patterns);
}
/**
* Creates a matcher that matches on any of the provided {@link PathPattern}s.
* @param pathPatterns the {@link PathPattern}s to match on
* @return the matcher to use
*/
public static ServerWebExchangeMatcher pathMatchers(PathPattern... pathPatterns) {
return pathMatchers(null, pathPatterns);
}
/**
* Creates a matcher that matches on the specific method and any of the provided
* {@link PathPattern}s.
* @param method the method to match on. If null, any method will be matched.
* @param pathPatterns the {@link PathPattern}s to match on
* @return the matcher to use
*/
public static ServerWebExchangeMatcher pathMatchers(HttpMethod method, PathPattern... pathPatterns) {
List<ServerWebExchangeMatcher> matchers = new ArrayList<>(pathPatterns.length);
for (PathPattern pathPattern : pathPatterns) {
matchers.add(new PathPatternParserServerWebExchangeMatcher(pathPattern, method));
}
return new OrServerWebExchangeMatcher(matchers);
}
/**
* Creates a matcher that will match on any of the provided matchers
* @param matchers the matchers to match on
@@ -122,4 +122,14 @@ public class DefaultSavedRequestTests {
assertThat(new URL(savedRequest.getRedirectUrl())).hasQuery("foo=bar&success");
}
// gh-13438
@Test
public void getRedirectUrlWhenQueryAlreadyHasSuccessThenDoesNotAdd() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setQueryString("foo=bar&success");
DefaultSavedRequest savedRequest = new DefaultSavedRequest(request, new MockPortResolver(8080, 8443),
"success");
assertThat(savedRequest.getRedirectUrl()).contains("foo=bar&success");
}
}
@@ -0,0 +1,40 @@
/*
* 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.web.server.util.matcher;
import org.junit.jupiter.api.Test;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PathPatternParserServerWebExchangeMatcher}
*
* @author Marcus da Coregio
*/
class PathPatternParserServerWebExchangeMatcherTests {
@Test
void matchesWhenConfiguredWithNoTrailingSlashAndPathContainsSlashThenMatches() {
PathPatternParserServerWebExchangeMatcher matcher = new PathPatternParserServerWebExchangeMatcher("user/**");
MockServerHttpRequest request = MockServerHttpRequest.get("/user/test").build();
assertThat(matcher.matches(MockServerWebExchange.from(request)).block().isMatch()).isTrue();
}
}