Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 612909ac4f | |||
| 28f98b3351 | |||
| ed96e2cddf | |||
| 7200f76ac1 | |||
| 094b71b329 | |||
| 491f0f647c | |||
| 1db8734101 | |||
| 4b44a2d924 | |||
| e8b9a35494 | |||
| 82e5f62079 | |||
| 30bc2634d7 | |||
| 13ca7ac4d4 | |||
| c4f061c63d | |||
| 2c6d053ceb | |||
| ff4745a461 | |||
| 38a7ffcc9b | |||
| 6f55fc60bb | |||
| d0491c07e9 | |||
| 486c5118d2 | |||
| d02ab50577 | |||
| 17d8293be1 | |||
| bb46a54270 | |||
| df239b6448 | |||
| a939f17890 | |||
| fe9bc26bdc | |||
| 7813a9ba26 | |||
| 7d58b9391a | |||
| dc3cb68c76 | |||
| 8555c100d7 | |||
| ae46531add | |||
| 0121b08a85 | |||
| 56292c9971 | |||
| 4fc938555e | |||
| e574415244 | |||
| c5da886310 | |||
| 3bef5fd3ed | |||
| b7a9a654f0 | |||
| e7201c48d1 | |||
| a642fdb004 | |||
| 991b398c55 | |||
| 40d61743b9 | |||
| 8895a66a2b | |||
| 80a5028f3f | |||
| c30bacac10 | |||
| a104dec30d | |||
| 488b6ea531 | |||
| ef04ea9d68 | |||
| b7be4c462c | |||
| aa8440e1ee | |||
| 6d3f1699c0 | |||
| bb72eed8d1 | |||
| 108075763b |
+11
-4
@@ -34,6 +34,14 @@ class RepositoryConventionPlugin implements Plugin<Project> {
|
||||
if (forceMavenRepositories?.contains('local')) {
|
||||
mavenLocal()
|
||||
}
|
||||
maven {
|
||||
name = 'shibboleth'
|
||||
url = 'https://build.shibboleth.net/nexus/content/repositories/releases/'
|
||||
content {
|
||||
includeGroupByRegex('org\\.opensaml.*')
|
||||
includeGroupByRegex('net\\.shibboleth.*')
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
if (isSnapshot) {
|
||||
maven {
|
||||
@@ -67,12 +75,11 @@ class RepositoryConventionPlugin implements Plugin<Project> {
|
||||
password project.artifactoryPassword
|
||||
}
|
||||
}
|
||||
content {
|
||||
excludeGroup('net.minidev')
|
||||
}
|
||||
url = 'https://repo.spring.io/release/'
|
||||
}
|
||||
maven {
|
||||
name = 'shibboleth'
|
||||
url = 'https://build.shibboleth.net/nexus/content/repositories/releases/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-3
@@ -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);
|
||||
|
||||
+7
-7
@@ -125,27 +125,27 @@ public class RepositoryConventionPluginTests {
|
||||
|
||||
private void assertSnapshotRepository(RepositoryHandler repositories) {
|
||||
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(5);
|
||||
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
|
||||
.isEqualTo("https://repo.spring.io/snapshot/");
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
|
||||
.isEqualTo("https://repo.spring.io/snapshot/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(3)).getUrl().toString())
|
||||
.isEqualTo("https://repo.spring.io/milestone/");
|
||||
}
|
||||
|
||||
private void assertMilestoneRepository(RepositoryHandler repositories) {
|
||||
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(4);
|
||||
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
|
||||
.isEqualTo("https://repo.spring.io/milestone/");
|
||||
}
|
||||
|
||||
private void assertReleaseRepository(RepositoryHandler repositories) {
|
||||
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(3);
|
||||
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
|
||||
.isEqualTo("https://repo.spring.io/release/");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@ apply plugin: 'io.spring.convention.spring-module'
|
||||
apply plugin: 'trang'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
repositories {
|
||||
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
management platform(project(":spring-security-dependencies"))
|
||||
// NB: Don't add other compile time dependencies to the config module as this breaks tooling
|
||||
|
||||
+4
@@ -36,6 +36,10 @@ class MethodSecurityAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
}
|
||||
|
||||
private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {
|
||||
String advisorName = prefix + "Advisor";
|
||||
if (registry.containsBeanDefinition(advisorName)) {
|
||||
return;
|
||||
}
|
||||
String interceptorName = prefix + "MethodInterceptor";
|
||||
if (!registry.containsBeanDefinition(interceptorName)) {
|
||||
return;
|
||||
|
||||
+81
-12
@@ -18,9 +18,14 @@ package org.springframework.security.config.annotation.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
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 +41,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 +303,73 @@ 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 = mappableServletRegistrations(servletContext);
|
||||
if (registrations.isEmpty()) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
if (registrations.size() > 1) {
|
||||
String errorMessage = computeErrorMessage(registrations.values());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
}
|
||||
return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0]));
|
||||
}
|
||||
|
||||
private Map<String, ? extends ServletRegistration> mappableServletRegistrations(ServletContext servletContext) {
|
||||
Map<String, ServletRegistration> mappable = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, ? extends ServletRegistration> entry : servletContext.getServletRegistrations()
|
||||
.entrySet()) {
|
||||
if (!entry.getValue().getMappings().isEmpty()) {
|
||||
mappable.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return mappable;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
String template = "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).\n\n"
|
||||
+ "This is because there is more than one mappable servlet in your servlet context: %s.\n\n"
|
||||
+ "For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.";
|
||||
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
|
||||
for (ServletRegistration registration : registrations) {
|
||||
mappings.put(registration.getClassName(), registration.getMappings());
|
||||
}
|
||||
return String.format(template, mappings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,12 +445,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 +459,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
|
||||
|
||||
+23
-3
@@ -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
|
||||
|
||||
+35
-1
@@ -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,144 @@
|
||||
/*
|
||||
* 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.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
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).addMapping("/");
|
||||
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;
|
||||
|
||||
private final Set<String> mappings = new LinkedHashSet<>();
|
||||
|
||||
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) {
|
||||
this.mappings.addAll(Arrays.asList(urlPatterns));
|
||||
return this.mappings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getMappings() {
|
||||
return this.mappings;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
-1
@@ -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.
|
||||
@@ -20,6 +20,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
@@ -64,6 +65,8 @@ import org.springframework.security.test.context.support.WithSecurityContextTest
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -418,6 +421,17 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse();
|
||||
}
|
||||
|
||||
// gh-13572
|
||||
@Test
|
||||
public void configureWhenBeanOverridingDisallowedThenWorks() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, BusinessServiceConfig.class)
|
||||
.postProcessor(disallowBeanOverriding()).autowire();
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
static class MethodSecurityServiceConfig {
|
||||
|
||||
|
||||
+50
-4
@@ -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,16 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
private TestRequestMatcherRegistry matcherRegistry;
|
||||
|
||||
private WebApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
public void setUp() throws Exception {
|
||||
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);
|
||||
mockMvcPresentClasspath(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,6 +192,44 @@ 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).addMapping("/");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/**");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenUnmappableServletsThenSkips() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
private void mockMvcIntrospector(boolean isPresent) {
|
||||
ApplicationContext context = this.matcherRegistry.getApplicationContext();
|
||||
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
@@ -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.35")
|
||||
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.15")
|
||||
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.
|
||||
|
||||
@@ -692,7 +692,7 @@ open class SecurityConfiguration {
|
||||
|
||||
=== Publish an `AuthenticationManager` Bean
|
||||
|
||||
As part of `WebSecurityConfigurerAdapeter` removal, `configure(AuthenticationManagerBuilder)` is also removed.
|
||||
As part of `WebSecurityConfigurerAdapter` removal, `configure(AuthenticationManagerBuilder)` is also removed.
|
||||
Preparing for its removal will differ based on your reason for using it.
|
||||
|
||||
==== LDAP Authentication
|
||||
|
||||
@@ -410,8 +410,8 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
[[webflux-headers-referrer]]
|
||||
== Referrer Policy
|
||||
|
||||
Spring Security does not add xref:features/exploits/headers.adoc#headers-referrer[Referrer Policy] headers by default.
|
||||
You can enable the Referrer Policy header using configuration as shown below:
|
||||
Spring Security adds the xref:features/exploits/headers.adoc#headers-referrer[Referrer Policy] header by default with the directive `no-referrer`.
|
||||
You can change the Referrer Policy header using configuration as shown below:
|
||||
|
||||
.Referrer Policy Configuration
|
||||
[tabs]
|
||||
|
||||
@@ -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>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -159,7 +159,39 @@ XML::
|
||||
|
||||
The following beans are required in an application context to enable remember-me services:
|
||||
|
||||
[source,xml]
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
RememberMeAuthenticationFilter rememberMeFilter() {
|
||||
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter();
|
||||
rememberMeFilter.setRememberMeServices(rememberMeServices());
|
||||
rememberMeFilter.setAuthenticationManager(theAuthenticationManager);
|
||||
return rememberMeFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
TokenBasedRememberMeServices rememberMeServices() {
|
||||
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices();
|
||||
rememberMeServices.setUserDetailsService(myUserDetailsService);
|
||||
rememberMeServices.setKey("springRocks");
|
||||
return rememberMeServices;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
|
||||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider();
|
||||
rememberMeAuthenticationProvider.setKey("springRocks");
|
||||
return rememberMeAuthenticationProvider;
|
||||
}
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<bean id="rememberMeFilter" class=
|
||||
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
|
||||
@@ -178,6 +210,7 @@ The following beans are required in an application context to enable remember-me
|
||||
<property name="key" value="springRocks"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
|
||||
Don't forget to add your `RememberMeServices` implementation to your `UsernamePasswordAuthenticationFilter.setRememberMeServices()` property, include the `RememberMeAuthenticationProvider` in your `AuthenticationManager.setProviders()` list, and add `RememberMeAuthenticationFilter` into your `FilterChainProxy` (typically immediately after your `UsernamePasswordAuthenticationFilter`).
|
||||
|
||||
|
||||
@@ -241,16 +241,15 @@ fun rest(): RestTemplate {
|
||||
val rest = RestTemplate()
|
||||
rest.interceptors.add(ClientHttpRequestInterceptor { request, body, execution ->
|
||||
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
|
||||
if (authentication != null) {
|
||||
execution.execute(request, body)
|
||||
if (authentication == null) {
|
||||
return execution.execute(request, body)
|
||||
}
|
||||
|
||||
if (authentication!!.credentials !is AbstractOAuth2Token) {
|
||||
execution.execute(request, body)
|
||||
if (authentication.credentials !is AbstractOAuth2Token) {
|
||||
return execution.execute(request, body)
|
||||
}
|
||||
|
||||
val token: AbstractOAuth2Token = authentication.credentials as AbstractOAuth2Token
|
||||
request.headers.setBearerAuth(token.tokenValue)
|
||||
request.headers.setBearerAuth(authentication.credentials.tokenValue)
|
||||
execution.execute(request, body)
|
||||
})
|
||||
return rest
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'org.antora' version '1.0.0'
|
||||
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
|
||||
id 'io.spring.convention.repository'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
@@ -40,7 +41,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 +50,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)
|
||||
}
|
||||
|
||||
@@ -57,10 +60,3 @@ def resolvedVersions(Configuration configuration) {
|
||||
.resolvedArtifacts
|
||||
.collectEntries { [(it.name + '-version'): it.moduleVersion.id.version] }
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/release' }
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
maven { url 'https://repo.spring.io/snapshot' }
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
aspectjVersion=1.9.19
|
||||
aspectjVersion=1.9.20
|
||||
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-SNAPSHOT
|
||||
version=5.8.6
|
||||
kotlinVersion=1.7.22
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
+3
-4
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
-4
@@ -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();
|
||||
|
||||
+26
-1
@@ -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
|
||||
|
||||
+10
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user