Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c17cfae36 | |||
| 342735043d | |||
| 779541b340 | |||
| 5b293d2116 | |||
| 18e88366d2 | |||
| aeafcc1377 | |||
| 48babb7efa | |||
| f026e29771 | |||
| 620e6e0c34 | |||
| 4ebfa2c804 | |||
| f03224fe7f | |||
| 3feb809b35 | |||
| 771d9cd8b6 | |||
| a580856bb2 | |||
| 9b7a110704 | |||
| db37bdfe94 | |||
| ce012a4661 | |||
| 629540f9d8 | |||
| 96d1763fc4 | |||
| 98f9306a7c | |||
| 8efbbb3eb5 | |||
| 681681cbb8 | |||
| 612909ac4f | |||
| 54f7eacae6 | |||
| 5fb6f5768c | |||
| 28f98b3351 | |||
| ed96e2cddf | |||
| 97f6ca6f82 | |||
| ecc20ceffd | |||
| e147b22130 | |||
| d2cddb7e2d | |||
| 67e1832922 | |||
| 0426d27698 | |||
| 7afadfc0ef | |||
| 5f2a8c46d7 | |||
| 7200f76ac1 | |||
| 094b71b329 | |||
| 491f0f647c | |||
| ca0140c586 | |||
| 17e9fec6eb | |||
| 1db8734101 | |||
| b5f42b509b | |||
| 90936537dc | |||
| a8fcfaa428 | |||
| c20100dab7 | |||
| 4b44a2d924 | |||
| 1f27b18398 | |||
| e8b9a35494 | |||
| c4b773b150 | |||
| 82e5f62079 | |||
| 4257a97504 | |||
| 30bc2634d7 | |||
| 461d6edd85 | |||
| 13ca7ac4d4 | |||
| ee13216882 | |||
| c4f061c63d | |||
| 0d2315cf3a | |||
| 2c6d053ceb | |||
| ff4745a461 | |||
| 76b5d17c83 | |||
| 38a7ffcc9b | |||
| 6f55fc60bb | |||
| d0491c07e9 | |||
| 486c5118d2 | |||
| d02ab50577 | |||
| 17d8293be1 | |||
| 0686b461af | |||
| fe7bee9236 | |||
| bb46a54270 | |||
| df239b6448 | |||
| cbef118026 | |||
| a939f17890 | |||
| fe9bc26bdc | |||
| 7813a9ba26 | |||
| da9b81b314 | |||
| 613ce98b0f | |||
| eb420b1095 | |||
| de222572b9 | |||
| e204a3bca6 | |||
| 96416a924b | |||
| 7d58b9391a | |||
| dc3cb68c76 | |||
| 8555c100d7 | |||
| ae46531add | |||
| 0121b08a85 | |||
| 56292c9971 | |||
| 4fc938555e | |||
| e574415244 | |||
| c5da886310 | |||
| 3bef5fd3ed | |||
| f5538be8d0 | |||
| b7a9a654f0 | |||
| e7201c48d1 | |||
| f0f6de435d | |||
| a642fdb004 | |||
| 991b398c55 | |||
| dbf0c66301 | |||
| 933b302979 | |||
| 6393702e70 | |||
| 83c0f4231e | |||
| 40d61743b9 | |||
| b5d63e3485 | |||
| 8895a66a2b | |||
| 863aa5f65f | |||
| b02594d451 | |||
| 1abfd2c801 | |||
| 6e2c9b421b | |||
| 80a5028f3f | |||
| 225f3536c5 | |||
| c30bacac10 | |||
| e8ab855d43 | |||
| 797b2a5473 | |||
| a104dec30d | |||
| 488b6ea531 | |||
| ef04ea9d68 | |||
| b7be4c462c | |||
| aa8440e1ee | |||
| a9723d7ce4 | |||
| 6d3f1699c0 | |||
| bb72eed8d1 | |||
| 108075763b |
+2
-1
@@ -1,4 +1,5 @@
|
||||
import io.spring.gradle.IncludeRepoTask
|
||||
import trang.RncToXsd
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
@@ -174,7 +175,7 @@ if (hasProperty('buildScan')) {
|
||||
|
||||
nohttp {
|
||||
source.exclude "buildSrc/build/**"
|
||||
|
||||
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
|
||||
}
|
||||
|
||||
tasks.register('cloneSamples', IncludeRepoTask) {
|
||||
|
||||
@@ -86,7 +86,7 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.7'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.9'
|
||||
implementation 'org.gretty:gretty:3.0.9'
|
||||
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
|
||||
@@ -34,7 +34,7 @@ class JacocoPlugin implements Plugin<Project> {
|
||||
project.tasks.check.dependsOn project.tasks.jacocoTestReport
|
||||
|
||||
project.jacoco {
|
||||
toolVersion = '0.8.7'
|
||||
toolVersion = '0.8.9'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.plugins.JavaPlugin
|
||||
import org.gradle.api.tasks.bundling.Zip
|
||||
import org.springframework.gradle.xsd.CreateVersionlessXsdTask
|
||||
|
||||
public class SchemaZipPlugin implements Plugin<Project> {
|
||||
|
||||
@@ -15,7 +16,10 @@ public class SchemaZipPlugin implements Plugin<Project> {
|
||||
schemaZip.archiveClassifier = 'schema'
|
||||
schemaZip.description = "Builds -${schemaZip.archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at static.springframework.org/schema."
|
||||
|
||||
def versionlessXsd = project.tasks.create("versionlessXsd", CreateVersionlessXsdTask) {
|
||||
description = "Generates spring-security.xsd"
|
||||
versionlessXsdFile = project.layout.buildDirectory.file("versionlessXsd/spring-security.xsd")
|
||||
}
|
||||
project.rootProject.subprojects.each { module ->
|
||||
|
||||
module.getPlugins().withType(JavaPlugin.class).all {
|
||||
@@ -36,17 +40,14 @@ public class SchemaZipPlugin implements Plugin<Project> {
|
||||
duplicatesStrategy 'exclude'
|
||||
from xsdFile.path
|
||||
}
|
||||
}
|
||||
File symlink = module.sourceSets.main.resources.find {
|
||||
it.path.endsWith('org/springframework/security/config/spring-security.xsd')
|
||||
}
|
||||
if (symlink != null) {
|
||||
schemaZip.into('security') {
|
||||
duplicatesStrategy 'exclude'
|
||||
from symlink.path
|
||||
}
|
||||
versionlessXsd.getInputFiles().from(xsdFile.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schemaZip.into("security") {
|
||||
from(versionlessXsd.getOutputs())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019-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.gradle.xsd;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.*;
|
||||
import org.gradle.work.DisableCachingByDefault;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Creates the spring-security.xsd automatically
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@DisableCachingByDefault(because = "not worth it")
|
||||
public abstract class CreateVersionlessXsdTask extends DefaultTask {
|
||||
|
||||
@InputFiles
|
||||
public abstract ConfigurableFileCollection getInputFiles();
|
||||
|
||||
@OutputFile
|
||||
abstract RegularFileProperty getVersionlessXsdFile();
|
||||
|
||||
@TaskAction
|
||||
void createVersionlessXsd() throws IOException {
|
||||
XsdFileMajorMinorVersion largest = null;
|
||||
ConfigurableFileCollection inputFiles = getInputFiles();
|
||||
if (inputFiles.isEmpty()) {
|
||||
throw new IllegalStateException("No Inputs configured");
|
||||
}
|
||||
for (File file : inputFiles) {
|
||||
XsdFileMajorMinorVersion current = XsdFileMajorMinorVersion.create(file);
|
||||
if (current == null) {
|
||||
continue;
|
||||
}
|
||||
if (largest == null) {
|
||||
largest = current;
|
||||
}
|
||||
else if (current.getVersion().isGreaterThan(largest.getVersion())) {
|
||||
largest = current;
|
||||
}
|
||||
}
|
||||
if (largest == null) {
|
||||
throw new IllegalStateException("Could not create versionless xsd file because no files matching spring-security-<digit>.xsd were found in " + inputFiles.getFiles());
|
||||
}
|
||||
Path to = getVersionlessXsdFile().getAsFile().get().toPath();
|
||||
Path from = largest.getFile().toPath();
|
||||
Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
}
|
||||
|
||||
static class XsdFileMajorMinorVersion {
|
||||
private final File file;
|
||||
|
||||
private final MajorMinorVersion version;
|
||||
|
||||
private XsdFileMajorMinorVersion(File file, MajorMinorVersion version) {
|
||||
this.file = file;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
private static final Pattern FILE_MAJOR_MINOR_VERSION_PATTERN = Pattern.compile("^spring-security-(\\d+)\\.(\\d+)\\.xsd$");
|
||||
|
||||
/**
|
||||
* If matches xsd with major minor version (e.g. spring-security-5.1.xsd returns it, otherwise null
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
static XsdFileMajorMinorVersion create(File file) {
|
||||
String fileName = file.getName();
|
||||
Matcher matcher = FILE_MAJOR_MINOR_VERSION_PATTERN.matcher(fileName);
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
int major = Integer.parseInt(matcher.group(1));
|
||||
int minor = Integer.parseInt(matcher.group(2));
|
||||
MajorMinorVersion version = new MajorMinorVersion(major, minor);
|
||||
return new XsdFileMajorMinorVersion(file, version);
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public MajorMinorVersion getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
static class MajorMinorVersion {
|
||||
private final int major;
|
||||
|
||||
private final int minor;
|
||||
|
||||
MajorMinorVersion(int major, int minor) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
}
|
||||
|
||||
public int getMajor() {
|
||||
return major;
|
||||
}
|
||||
|
||||
public int getMinor() {
|
||||
return minor;
|
||||
}
|
||||
|
||||
public boolean isGreaterThan(MajorMinorVersion version) {
|
||||
if (getMajor() > version.getMajor()) {
|
||||
return true;
|
||||
}
|
||||
if (getMajor() < version.getMajor()) {
|
||||
return false;
|
||||
}
|
||||
if (getMinor() > version.getMinor()) {
|
||||
return true;
|
||||
}
|
||||
if (getMinor() < version.getMinor()) {
|
||||
return false;
|
||||
}
|
||||
// they are equal
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+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/");
|
||||
}
|
||||
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2019-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.gradle.xsd;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.gradle.xsd.CreateVersionlessXsdTask.MajorMinorVersion;
|
||||
import org.springframework.gradle.xsd.CreateVersionlessXsdTask.XsdFileMajorMinorVersion;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class CreateVersionlessXsdTaskTests {
|
||||
|
||||
@Test
|
||||
void xsdCreateWhenValid() {
|
||||
File file = new File("spring-security-2.0.xsd");
|
||||
XsdFileMajorMinorVersion xsdFile = XsdFileMajorMinorVersion.create(file);
|
||||
assertThat(xsdFile).isNotNull();
|
||||
assertThat(xsdFile.getFile()).isEqualTo(file);
|
||||
assertThat(xsdFile.getVersion().getMajor()).isEqualTo(2);
|
||||
assertThat(xsdFile.getVersion().getMinor()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void xsdCreateWhenPatchReleaseThenNull() {
|
||||
File file = new File("spring-security-2.0.1.xsd");
|
||||
XsdFileMajorMinorVersion xsdFile = XsdFileMajorMinorVersion.create(file);
|
||||
assertThat(xsdFile).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void xsdCreateWhenNotXsdFileThenNull() {
|
||||
File file = new File("spring-security-2.0.txt");
|
||||
XsdFileMajorMinorVersion xsdFile = XsdFileMajorMinorVersion.create(file);
|
||||
assertThat(xsdFile).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void xsdCreateWhenNotStartWithSpringSecurityThenNull() {
|
||||
File file = new File("spring-securityNO-2.0.xsd");
|
||||
XsdFileMajorMinorVersion xsdFile = XsdFileMajorMinorVersion.create(file);
|
||||
assertThat(xsdFile).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isGreaterWhenMajorLarger() {
|
||||
MajorMinorVersion larger = new MajorMinorVersion(2,0);
|
||||
MajorMinorVersion smaller = new MajorMinorVersion(1,0);
|
||||
assertThat(larger.isGreaterThan(smaller)).isTrue();
|
||||
assertThat(smaller.isGreaterThan(larger)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isGreaterWhenMinorLarger() {
|
||||
MajorMinorVersion larger = new MajorMinorVersion(1,1);
|
||||
MajorMinorVersion smaller = new MajorMinorVersion(1,0);
|
||||
assertThat(larger.isGreaterThan(smaller)).isTrue();
|
||||
assertThat(smaller.isGreaterThan(larger)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isGreaterWhenMajorAndMinorLarger() {
|
||||
MajorMinorVersion larger = new MajorMinorVersion(2,1);
|
||||
MajorMinorVersion smaller = new MajorMinorVersion(1,0);
|
||||
assertThat(larger.isGreaterThan(smaller)).isTrue();
|
||||
assertThat(smaller.isGreaterThan(larger)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isGreaterWhenSame() {
|
||||
MajorMinorVersion first = new MajorMinorVersion(1,0);
|
||||
MajorMinorVersion second = new MajorMinorVersion(1,0);
|
||||
assertThat(first.isGreaterThan(second)).isFalse();
|
||||
assertThat(second.isGreaterThan(first)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.springframework.gradle.xsd.CreateVersionlessXsdTask
|
||||
import trang.RncToXsd
|
||||
|
||||
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
|
||||
@@ -118,13 +116,31 @@ dependencies {
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
}
|
||||
|
||||
def versionlessXsd = project.tasks.create("versionlessXsd", CreateVersionlessXsdTask) {
|
||||
inputFiles.from(project.sourceSets.main.resources)
|
||||
versionlessXsdFile = project.layout.buildDirectory.file("versionlessXsd/spring-security.xsd")
|
||||
}
|
||||
|
||||
rncToXsd {
|
||||
processResources {
|
||||
from(versionlessXsd) {
|
||||
into 'org/springframework/security/config/'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('rncToXsd', RncToXsd).configure {
|
||||
rncDir = file('src/main/resources/org/springframework/security/config/')
|
||||
xsdDir = rncDir
|
||||
xslFile = new File(rncDir, 'spring-security.xsl')
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDir(tasks.named('rncToXsd'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
languageVersion = "1.7"
|
||||
@@ -133,5 +149,3 @@ tasks.withType(KotlinCompile).configureEach {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
build.dependsOn rncToXsd
|
||||
|
||||
+3
-2
@@ -17,9 +17,9 @@
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -46,7 +46,8 @@ final class Jsr250MethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor jsr250AuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
static MethodInterceptor jsr250AuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
Jsr250AuthorizationManager jsr250 = new Jsr250AuthorizationManager();
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
class MethodSecurityAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
|
||||
registerAsAdvisor("preFilterAuthorization", registry);
|
||||
registerAsAdvisor("preAuthorizeAuthorization", registry);
|
||||
registerAsAdvisor("postFilterAuthorization", registry);
|
||||
registerAsAdvisor("postAuthorizeAuthorization", registry);
|
||||
registerAsAdvisor("securedAuthorization", registry);
|
||||
registerAsAdvisor("jsr250Authorization", registry);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
BeanDefinition definition = registry.getBeanDefinition(interceptorName);
|
||||
if (!(definition instanceof RootBeanDefinition)) {
|
||||
return;
|
||||
}
|
||||
RootBeanDefinition advisor = new RootBeanDefinition((RootBeanDefinition) definition);
|
||||
advisor.setTargetType(Advisor.class);
|
||||
registry.registerBeanDefinition(prefix + "Advisor", advisor);
|
||||
}
|
||||
|
||||
}
|
||||
+3
-2
@@ -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.
|
||||
@@ -60,7 +60,8 @@ final class MethodSecuritySelector implements ImportSelector {
|
||||
|
||||
private static final class AutoProxyRegistrarSelector extends AdviceModeImportSelector<EnableMethodSecurity> {
|
||||
|
||||
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
|
||||
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName(),
|
||||
MethodSecurityAdvisorRegistrar.class.getName() };
|
||||
|
||||
private static final String[] ASPECTJ_IMPORTS = new String[] {
|
||||
MethodSecurityAspectJAutoProxyRegistrar.class.getName() };
|
||||
|
||||
+61
-10
@@ -16,15 +16,21 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
@@ -36,7 +42,9 @@ import org.springframework.security.authorization.method.PostFilterAuthorization
|
||||
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* Base {@link Configuration} for enabling Spring Security Method Security.
|
||||
@@ -52,26 +60,28 @@ final class PrePostMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preFilterAuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
static MethodInterceptor preFilterAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
|
||||
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
|
||||
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
|
||||
preFilter.setExpressionHandler(
|
||||
expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
return preFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preAuthorizeAuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
|
||||
manager.setExpressionHandler(
|
||||
expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
|
||||
.preAuthorize(manager(manager, registryProvider));
|
||||
strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
|
||||
@@ -81,14 +91,15 @@ final class PrePostMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postAuthorizeAuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
|
||||
manager.setExpressionHandler(
|
||||
expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
|
||||
.postAuthorize(manager(manager, registryProvider));
|
||||
strategyProvider.ifAvailable(postAuthorize::setSecurityContextHolderStrategy);
|
||||
@@ -98,13 +109,14 @@ final class PrePostMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postFilterAuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
static MethodInterceptor postFilterAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
|
||||
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
|
||||
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
|
||||
postFilter.setExpressionHandler(
|
||||
expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
return postFilter;
|
||||
}
|
||||
|
||||
@@ -121,4 +133,43 @@ final class PrePostMethodSecurityConfiguration {
|
||||
return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
|
||||
}
|
||||
|
||||
private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
|
||||
|
||||
private final Supplier<MethodSecurityExpressionHandler> expressionHandler;
|
||||
|
||||
private DeferringMethodSecurityExpressionHandler(
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext applicationContext) {
|
||||
this.expressionHandler = SingletonSupplier.of(() -> expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, applicationContext)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionParser getExpressionParser() {
|
||||
return this.expressionHandler.get().getExpressionParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationContext createEvaluationContext(Authentication authentication, MethodInvocation invocation) {
|
||||
return this.expressionHandler.get().createEvaluationContext(authentication, invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
|
||||
MethodInvocation invocation) {
|
||||
return this.expressionHandler.get().createEvaluationContext(authentication, invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) {
|
||||
return this.expressionHandler.get().filter(filterTarget, filterExpression, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReturnObject(Object returnObject, EvaluationContext ctx) {
|
||||
this.expressionHandler.get().setReturnObject(returnObject, ctx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -17,9 +17,9 @@
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -46,7 +46,8 @@ final class SecuredMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor securedAuthorizationMethodInterceptor(ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
static MethodInterceptor securedAuthorizationMethodInterceptor(
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
SecuredAuthorizationManager secured = new SecuredAuthorizationManager();
|
||||
SecurityContextHolderStrategy strategy = strategyProvider
|
||||
|
||||
+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 jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.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;
|
||||
|
||||
/**
|
||||
@@ -179,14 +185,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,12 +327,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,6 +341,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
|
||||
|
||||
+8
-5
@@ -269,6 +269,9 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
}
|
||||
}
|
||||
this.initDefaultLoginFilter(http);
|
||||
if (this.authenticationManager == null) {
|
||||
registerDefaultAuthenticationProvider(http);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,10 +287,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
filter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
|
||||
http.addFilter(postProcess(filter));
|
||||
super.configure(http);
|
||||
if (this.authenticationManager == null) {
|
||||
registerDefaultAuthenticationProvider(http);
|
||||
}
|
||||
else {
|
||||
if (this.authenticationManager != null) {
|
||||
this.saml2WebSsoAuthenticationFilter.setAuthenticationManager(this.authenticationManager);
|
||||
}
|
||||
}
|
||||
@@ -361,7 +361,10 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
}
|
||||
|
||||
private void registerDefaultAuthenticationProvider(B http) {
|
||||
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
|
||||
OpenSaml4AuthenticationProvider provider = getBeanOrNull(http, OpenSaml4AuthenticationProvider.class);
|
||||
if (provider == null) {
|
||||
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
|
||||
}
|
||||
}
|
||||
|
||||
private void registerDefaultCsrfOverride(B http) {
|
||||
|
||||
+2
-2
@@ -781,8 +781,8 @@ class HttpConfigurationBuilder {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterSecurityInterceptor.class);
|
||||
builder.addPropertyReference("accessDecisionManager", accessManagerId);
|
||||
builder.addPropertyValue("authenticationManager", authManager);
|
||||
if ("false".equals(this.httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) {
|
||||
builder.addPropertyValue("observeOncePerRequest", Boolean.FALSE);
|
||||
if ("true".equals(this.httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) {
|
||||
builder.addPropertyValue("observeOncePerRequest", Boolean.TRUE);
|
||||
}
|
||||
builder.addPropertyValue("securityMetadataSource", securityMds);
|
||||
builder.addPropertyValue("securityContextHolderStrategy", this.holderStrategyRef);
|
||||
|
||||
+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.
|
||||
@@ -191,9 +191,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
|
||||
@@ -1573,6 +1575,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];
|
||||
@@ -1593,6 +1607,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();
|
||||
|
||||
@@ -1600,6 +1616,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
|
||||
@@ -1619,6 +1637,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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
spring-security-6.0.xsd
|
||||
@@ -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 jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.ServletRegistration;
|
||||
import jakarta.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);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
static class MethodSecurityServiceConfig {
|
||||
|
||||
+48
-3
@@ -19,18 +19,22 @@ package org.springframework.security.config.annotation.web;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Servlet;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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;
|
||||
@@ -54,12 +58,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);
|
||||
mockMvcIntrospector(true);
|
||||
}
|
||||
|
||||
@@ -147,6 +154,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
@@ -28,10 +28,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.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
@@ -77,7 +77,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
@@ -32,7 +32,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.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
@@ -220,7 +220,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
@@ -30,8 +30,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.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+34
@@ -43,6 +43,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
@@ -60,6 +61,7 @@ import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2Utils;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
@@ -358,6 +360,15 @@ public class Saml2LoginConfigurerTests {
|
||||
.andExpect(redirectedUrl("http://localhost/saml2/authenticate/registration-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saml2LoginWhenCustomAuthenticationProviderThenUses() throws Exception {
|
||||
this.spring.register(CustomAuthenticationProviderConfig.class).autowire();
|
||||
AuthenticationProvider provider = this.spring.getContext().getBean(AuthenticationProvider.class);
|
||||
this.mvc.perform(post("/login/saml2/sso/registration-id").param("SAMLResponse", SIGNED_RESPONSE))
|
||||
.andExpect(status().isFound());
|
||||
verify(provider).authenticate(any());
|
||||
}
|
||||
|
||||
private void performSaml2Login(String expected) throws IOException, ServletException {
|
||||
// setup authentication parameters
|
||||
this.request.setRequestURI("/login/saml2/sso/registration-id");
|
||||
@@ -668,6 +679,29 @@ public class Saml2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
static class CustomAuthenticationProviderConfig {
|
||||
|
||||
private final OpenSaml4AuthenticationProvider provider = spy(new OpenSaml4AuthenticationProvider());
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.saml2Login(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthenticationProvider provider() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Saml2LoginConfigBeans {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -323,6 +323,13 @@ public class MiscHttpConfigTests {
|
||||
assertThat(filterSecurityInterceptor.isObserveOncePerRequest()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenOncePerRequestIsTrueThenFilterSecurityInterceptorObserveOncePerRequestIsTrue() {
|
||||
this.spring.configLocations(xml("OncePerRequestTrue")).autowire();
|
||||
FilterSecurityInterceptor filterSecurityInterceptor = getFilter(FilterSecurityInterceptor.class);
|
||||
assertThat(filterSecurityInterceptor.isObserveOncePerRequest()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenCustomHttpBasicEntryPointRefThenInvokesOnCommence() throws Exception {
|
||||
this.spring.configLocations(xml("CustomHttpBasicEntryPointRef")).autowire();
|
||||
|
||||
+3
-3
@@ -28,8 +28,8 @@ import jakarta.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;
|
||||
@@ -132,7 +132,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();
|
||||
}
|
||||
@@ -140,7 +140,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);
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http once-per-request="true" use-authorization-manager="false">
|
||||
<http-basic/>
|
||||
<intercept-url pattern="/protected" access="authenticated"/>
|
||||
<intercept-url pattern="/unprotected-forwards-to-protected" access="permitAll"/>
|
||||
</http>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
<b:import resource="handlermappingintrospector.xml"/>
|
||||
</b:beans>
|
||||
+14
-2
@@ -41,11 +41,17 @@ public final class AuthorizationManagers {
|
||||
List<AuthorizationDecision> decisions = new ArrayList<>();
|
||||
for (AuthorizationManager<T> manager : managers) {
|
||||
AuthorizationDecision decision = manager.check(authentication, object);
|
||||
if (decision == null || decision.isGranted()) {
|
||||
if (decision == null) {
|
||||
continue;
|
||||
}
|
||||
if (decision.isGranted()) {
|
||||
return decision;
|
||||
}
|
||||
decisions.add(decision);
|
||||
}
|
||||
if (decisions.isEmpty()) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
return new CompositeAuthorizationDecision(false, decisions);
|
||||
};
|
||||
}
|
||||
@@ -64,11 +70,17 @@ public final class AuthorizationManagers {
|
||||
List<AuthorizationDecision> decisions = new ArrayList<>();
|
||||
for (AuthorizationManager<T> manager : managers) {
|
||||
AuthorizationDecision decision = manager.check(authentication, object);
|
||||
if (decision != null && !decision.isGranted()) {
|
||||
if (decision == null) {
|
||||
continue;
|
||||
}
|
||||
if (!decision.isGranted()) {
|
||||
return decision;
|
||||
}
|
||||
decisions.add(decision);
|
||||
}
|
||||
if (decisions.isEmpty()) {
|
||||
return new AuthorizationDecision(true);
|
||||
}
|
||||
return new CompositeAuthorizationDecision(true, decisions);
|
||||
};
|
||||
}
|
||||
|
||||
+6
-3
@@ -36,12 +36,14 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
// gh-13069
|
||||
@Test
|
||||
void checkAnyOfWhenOneAbstainedThenAbstainedDecision() {
|
||||
void checkAnyOfWhenAllNonAbstainingDeniesThenDeniedDecision() {
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf((a, o) -> new AuthorizationDecision(false),
|
||||
(a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNull();
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -61,8 +63,9 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
// gh-13069
|
||||
@Test
|
||||
void checkAllOfWhenOneAbstainedThenGrantedDecision() {
|
||||
void checkAllOfWhenAllNonAbstainingGrantsThenGrantedDecision() {
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf((a, o) -> new AuthorizationDecision(true),
|
||||
(a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
|
||||
+6
-6
@@ -12,12 +12,12 @@ dependencies {
|
||||
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:2022.0.7")
|
||||
api platform("org.springframework.data:spring-data-bom:2022.0.9")
|
||||
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.14.3")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.4.8"
|
||||
api "ch.qos.logback:logback-classic:1.4.11"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.24.4"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.43.3"
|
||||
@@ -25,7 +25,7 @@ dependencies {
|
||||
api "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
api "com.unboundid:unboundid-ldapsdk:6.0.9"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.13.5"
|
||||
api "io.mockk:mockk:1.13.7"
|
||||
api "io.micrometer:micrometer-observation:$micrometerVersion"
|
||||
api "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
api "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
@@ -34,8 +34,8 @@ dependencies {
|
||||
api "jakarta.servlet:jakarta.servlet-api:6.0.0"
|
||||
api "jakarta.xml.bind:jakarta.xml.bind-api:4.0.0"
|
||||
api "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
api "jakarta.websocket:jakarta.websocket-api:2.1.0"
|
||||
api "jakarta.websocket:jakarta.websocket-client-api:2.1.0"
|
||||
api "jakarta.websocket:jakarta.websocket-api:2.1.1"
|
||||
api "jakarta.websocket:jakarta.websocket-client-api:2.1.1"
|
||||
api "ldapsdk:ldapsdk:4.1"
|
||||
api "net.sourceforge.htmlunit:htmlunit:2.66.0"
|
||||
api "org.apache.directory.server:apacheds-core-entry:1.5.5"
|
||||
@@ -67,7 +67,7 @@ dependencies {
|
||||
api "org.skyscreamer:jsonassert:1.5.1"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
api "org.slf4j:slf4j-api:2.0.7"
|
||||
api "org.springframework.ldap:spring-ldap-core:3.0.4"
|
||||
api "org.springframework.ldap:spring-ldap-core:3.0.5"
|
||||
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
api 'org.apache.maven.resolver:maven-resolver-connector-basic:1.8.2'
|
||||
api 'org.apache.maven.resolver:maven-resolver-impl:1.8.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ Without proper configuration, the application server can not know that the load
|
||||
|
||||
To fix this, you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
|
||||
To make the application aware of this, you need to configure your application server to be aware of the X-Forwarded headers.
|
||||
For example, Tomcat uses https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[`RemoteIpValve`] and Jetty uses https://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[`ForwardedRequestCustomizer`].
|
||||
For example, Tomcat uses https://tomcat.apache.org/tomcat-10.1-doc/api/org/apache/catalina/valves/RemoteIpValve.html[`RemoteIpValve`] and Jetty uses https://eclipse.dev/jetty/javadoc/jetty-11/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[`ForwardedRequestCustomizer`].
|
||||
Alternatively, Spring users can use https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java[`ForwardedHeaderFilter`].
|
||||
|
||||
Spring Boot users can use the `server.use-forward-headers` property to configure the application.
|
||||
|
||||
@@ -191,7 +191,7 @@ Alternatively, you can manually add the starter:
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-security"
|
||||
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||
}
|
||||
----
|
||||
|
||||
@@ -245,8 +245,8 @@ A minimal Spring Security Maven set of dependencies typically looks like the fol
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
compile "org.springframework.security:spring-security-web"
|
||||
compile "org.springframework.security:spring-security-config"
|
||||
implementation "org.springframework.security:spring-security-web"
|
||||
implementation "org.springframework.security:spring-security-config"
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -448,8 +448,8 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
[[webflux-headers-referrer]]
|
||||
== Referrer Policy
|
||||
|
||||
By default, Spring Security does not add xref:features/exploits/headers.adoc#headers-referrer[Referrer Policy] headers.
|
||||
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]
|
||||
|
||||
@@ -7,7 +7,7 @@ Without proper configuration, the application server will not know that the load
|
||||
|
||||
To fix this you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
|
||||
To make the application aware of this, you need to either configure your application server aware of the X-Forwarded headers.
|
||||
For example Tomcat uses the https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
|
||||
For example Tomcat uses the https://tomcat.apache.org/tomcat-10.1-doc/api/org/apache/catalina/valves/RemoteIpValve.html[RemoteIpValve] and Jetty uses https://eclipse.dev/jetty/javadoc/jetty-11/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[ForwardedRequestCustomizer].
|
||||
Alternatively, Spring 4.3+ users can leverage https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java[ForwardedHeaderFilter].
|
||||
|
||||
Spring Boot users may use the `server.use-forward-headers` property to configure the application.
|
||||
|
||||
@@ -167,44 +167,222 @@ In fact, a `SecurityFilterChain` might have zero security `Filter` instances if
|
||||
== Security Filters
|
||||
|
||||
The Security Filters are inserted into the <<servlet-filterchainproxy>> with the <<servlet-securityfilterchain>> API.
|
||||
The <<servlet-filters-review,order of `Filter`>> instances matters.
|
||||
It is typically not necessary to know the ordering of Spring Security's `Filter` instances.
|
||||
However, there are times that it is beneficial to know the ordering.
|
||||
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, 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].
|
||||
|
||||
The following 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`]
|
||||
* `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
|
||||
@@ -336,3 +514,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>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -157,7 +157,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">
|
||||
@@ -176,6 +208,7 @@ The following beans are required in an application context to enable remember-me
|
||||
<property name="key" value="springRocks"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
|
||||
Remember 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`).
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ It maps the certificate to an application user and loads that user's set of gran
|
||||
|
||||
You can also use SSL with "`mutual authentication`". The server then requests a valid certificate from the client as part of the SSL handshake.
|
||||
The server authenticates the client by checking that its certificate is signed by an acceptable authority.
|
||||
For example, if you use Tomcat, you should read the https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html[Tomcat SSL instructions].
|
||||
For example, if you use Tomcat, you should read the https://tomcat.apache.org/tomcat-10.1-doc/ssl-howto.html[Tomcat SSL instructions].
|
||||
You should get this working before trying it out with Spring Security.
|
||||
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ public class Config {
|
||||
|
||||
The code is invoked in the following order:
|
||||
|
||||
* Code in the `Config.configure` method is invoked
|
||||
* Code in the `Config.filterChain` method is invoked
|
||||
* Code in the `MyCustomDsl.init` method is invoked
|
||||
* Code in the `MyCustomDsl.configure` method is invoked
|
||||
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
aspectjVersion=1.9.19
|
||||
reactorVersion=2022.0.8
|
||||
aspectjVersion=1.9.20
|
||||
reactorVersion=2022.0.10
|
||||
springJavaformatVersion=0.0.39
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=6.0.10
|
||||
micrometerVersion=1.10.8
|
||||
springBootVersion=3.0.8
|
||||
springFrameworkVersion=6.0.11
|
||||
micrometerVersion=1.10.10
|
||||
openSamlVersion=4.1.1
|
||||
version=6.0.4
|
||||
version=6.0.7
|
||||
kotlinVersion=1.7.22
|
||||
samplesBranch=main
|
||||
samplesBranch=6.0.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -205,6 +205,12 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
||||
Vendored
+8
-6
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,7 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
+2
-1
@@ -115,7 +115,8 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
|
||||
if (!supportedClientAuthenticationMethod) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"This class supports `client_secret_basic`, `client_secret_post`, and `none` by default. Client [%s] is using [%s] instead. Please use a supported client authentication method, or use `set/addParametersConverter` or `set/addHeadersConverter` to supply an instance that supports [%s].",
|
||||
clientRegistration.getRegistrationId(), clientAuthenticationMethod, clientAuthenticationMethod));
|
||||
clientRegistration.getRegistrationId(), clientAuthenticationMethod.getValue(),
|
||||
clientAuthenticationMethod.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ class ClientAuthenticationMethodValidatingRequestEntityConverter<T extends Abstr
|
||||
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST);
|
||||
Assert.isTrue(supportedClientAuthenticationMethod, () -> String.format(
|
||||
"This class supports `client_secret_basic`, `client_secret_post`, and `none` by default. Client [%s] is using [%s] instead. Please use a supported client authentication method, or use `setRequestEntityConverter` to supply an instance that supports [%s].",
|
||||
registrationId, clientAuthenticationMethod, clientAuthenticationMethod));
|
||||
registrationId, clientAuthenticationMethod.getValue(), clientAuthenticationMethod.getValue()));
|
||||
return this.delegate.convert(grantRequest);
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -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.
|
||||
@@ -30,16 +30,23 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
* {@link OAuth2AuthenticationException}.
|
||||
*
|
||||
* @author Dennis Neufeld
|
||||
* @author Steve Riesenberg
|
||||
* @since 5.3.4
|
||||
* @see OAuth2AuthenticationException
|
||||
* @see OAuth2ClientJackson2Module
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace", "suppressedExceptions" })
|
||||
abstract class OAuth2AuthenticationExceptionMixin {
|
||||
|
||||
@JsonProperty("error")
|
||||
abstract OAuth2Error getError();
|
||||
|
||||
@JsonProperty("detailMessage")
|
||||
abstract String getMessage();
|
||||
|
||||
@JsonCreator
|
||||
OAuth2AuthenticationExceptionMixin(@JsonProperty("error") OAuth2Error error,
|
||||
@JsonProperty("detailMessage") String message) {
|
||||
|
||||
+8
-2
@@ -34,10 +34,16 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
||||
* @see Saml2Jackson2Module
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace", "suppressedExceptions" })
|
||||
class Saml2AuthenticationExceptionMixin {
|
||||
abstract class Saml2AuthenticationExceptionMixin {
|
||||
|
||||
@JsonProperty("error")
|
||||
abstract Saml2Error getSaml2Error();
|
||||
|
||||
@JsonProperty("detailMessage")
|
||||
abstract String getMessage();
|
||||
|
||||
@JsonCreator
|
||||
Saml2AuthenticationExceptionMixin(@JsonProperty("error") Saml2Error error,
|
||||
|
||||
+4
-3
@@ -188,7 +188,7 @@ public class AuthorizationFilter extends GenericFilterBean {
|
||||
|
||||
/**
|
||||
* Sets whether this filter apply only once per request. By default, this is
|
||||
* <code>true</code>, meaning the filter will only execute once per request. Sometimes
|
||||
* <code>false</code>, meaning the filter will execute on every request. Sometimes
|
||||
* users may wish it to execute more than once per request, such as when JSP forwards
|
||||
* are being used and filter security is desired on each included fragment of the HTTP
|
||||
* request.
|
||||
@@ -200,7 +200,8 @@ public class AuthorizationFilter extends GenericFilterBean {
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to true, the filter will be applied to error dispatcher. Defaults to false.
|
||||
* If set to true, the filter will be applied to error dispatcher. Defaults to
|
||||
* {@code true}.
|
||||
* @param filterErrorDispatch whether the filter should be applied to error dispatcher
|
||||
*/
|
||||
public void setFilterErrorDispatch(boolean filterErrorDispatch) {
|
||||
@@ -209,7 +210,7 @@ public class AuthorizationFilter extends GenericFilterBean {
|
||||
|
||||
/**
|
||||
* If set to true, the filter will be applied to the async dispatcher. Defaults to
|
||||
* false.
|
||||
* {@code true}.
|
||||
* @param filterAsyncDispatch whether the filter should be applied to async dispatch
|
||||
*/
|
||||
public void setFilterAsyncDispatch(boolean filterAsyncDispatch) {
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint,
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException {
|
||||
response.addHeader("WWW-Authenticate", "Basic realm=\"" + this.realmName + "\"");
|
||||
response.setHeader("WWW-Authenticate", "Basic realm=\"" + this.realmName + "\"");
|
||||
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.web.savedrequest;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -77,7 +78,7 @@ public class CookieRequestCache implements RequestCache {
|
||||
int port = getPort(uriComponents);
|
||||
return builder.setScheme(uriComponents.getScheme()).setServerName(uriComponents.getHost())
|
||||
.setRequestURI(uriComponents.getPath()).setQueryString(uriComponents.getQuery()).setServerPort(port)
|
||||
.setMethod(request.getMethod()).build();
|
||||
.setMethod(request.getMethod()).setLocales(Collections.list(request.getLocales())).build();
|
||||
}
|
||||
|
||||
private int getPort(UriComponents uriComponents) {
|
||||
|
||||
+3
-4
@@ -37,6 +37,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}.
|
||||
@@ -371,10 +372,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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-5
@@ -28,6 +28,8 @@ import org.springframework.security.web.PortResolverImpl;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* {@code RequestCache} which stores the {@code SavedRequest} in the HttpSession.
|
||||
@@ -100,11 +102,14 @@ public class HttpSessionRequestCache implements RequestCache {
|
||||
|
||||
@Override
|
||||
public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (this.matchingRequestParameterName != null
|
||||
&& request.getParameter(this.matchingRequestParameterName) == null) {
|
||||
this.logger.trace(
|
||||
"matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided");
|
||||
return null;
|
||||
if (this.matchingRequestParameterName != null) {
|
||||
if (!StringUtils.hasText(request.getQueryString())
|
||||
|| !UriComponentsBuilder.fromUriString(UrlUtils.buildRequestUrl(request)).build().getQueryParams()
|
||||
.containsKey(this.matchingRequestParameterName)) {
|
||||
this.logger.trace(
|
||||
"matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
SavedRequest saved = getRequest(request, response);
|
||||
if (saved == null) {
|
||||
|
||||
+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
|
||||
|
||||
+19
@@ -16,8 +16,12 @@
|
||||
|
||||
package org.springframework.security.web.authentication.www;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
@@ -61,4 +65,19 @@ public class BasicAuthenticationEntryPointTests {
|
||||
assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Basic realm=\"hello\"");
|
||||
}
|
||||
|
||||
// gh-13737
|
||||
@Test
|
||||
void commenceWhenResponseHasHeaderThenOverride() throws IOException {
|
||||
BasicAuthenticationEntryPoint ep = new BasicAuthenticationEntryPoint();
|
||||
ep.setRealmName("hello");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRequestURI("/some_path");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"test\"");
|
||||
ep.commence(request, response, new DisabledException("Disabled"));
|
||||
List<String> headers = response.getHeaders("WWW-Authenticate");
|
||||
assertThat(headers).hasSize(1);
|
||||
assertThat(headers.get(0)).isEqualTo("Basic realm=\"hello\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+23
-1
@@ -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.
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
package org.springframework.security.web.savedrequest;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -182,6 +185,25 @@ public class CookieRequestCacheTests {
|
||||
assertThat(expiredCookie.getMaxAge()).isZero();
|
||||
}
|
||||
|
||||
// gh-13792
|
||||
@Test
|
||||
public void matchingRequestWhenMatchThenKeepOriginalRequestLocale() {
|
||||
CookieRequestCache cookieRequestCache = new CookieRequestCache();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setServerPort(443);
|
||||
request.setSecure(true);
|
||||
request.setScheme("https");
|
||||
request.setServerName("example.com");
|
||||
request.setRequestURI("/destination");
|
||||
request.setPreferredLocales(Arrays.asList(Locale.FRENCH, Locale.GERMANY));
|
||||
String redirectUrl = "https://example.com/destination";
|
||||
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie(redirectUrl)));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpServletRequest matchingRequest = cookieRequestCache.getMatchingRequest(request, response);
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
assertThat(Collections.list(matchingRequest.getLocales())).contains(Locale.FRENCH, Locale.GERMANY);
|
||||
}
|
||||
|
||||
private static String encodeCookie(String cookieValue) {
|
||||
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
|
||||
}
|
||||
|
||||
+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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+19
-5
@@ -32,8 +32,9 @@ import org.springframework.security.web.PortResolverImpl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
@@ -100,7 +101,7 @@ public class HttpSessionRequestCacheTests {
|
||||
public void getMatchingRequestWhenMatchingRequestParameterNameSetThenSessionNotAccessed() {
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
cache.setMatchingRequestParameterName("success");
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletRequest request = spy(new MockHttpServletRequest());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(request, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNull();
|
||||
verify(request, never()).getSession();
|
||||
@@ -115,7 +116,6 @@ public class HttpSessionRequestCacheTests {
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setQueryString("success"); // gh-12665
|
||||
requestToMatch.setParameter("success", "");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
@@ -131,7 +131,6 @@ public class HttpSessionRequestCacheTests {
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setQueryString("param=true&success");
|
||||
requestToMatch.setParameter("success", "");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
@@ -146,13 +145,28 @@ public class HttpSessionRequestCacheTests {
|
||||
assertThat(request.getSession().getAttribute(HttpSessionRequestCache.SAVED_REQUEST)).isNotNull();
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setQueryString("success");
|
||||
requestToMatch.setParameter("success", "");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
assertThat(request.getSession().getAttribute(HttpSessionRequestCache.SAVED_REQUEST)).isNull();
|
||||
}
|
||||
|
||||
// gh-13731
|
||||
@Test
|
||||
public void getMatchingRequestWhenMatchingRequestParameterNameSetThenDoesNotInvokeGetParameterMethods() {
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
cache.setMatchingRequestParameterName("success");
|
||||
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
|
||||
mockRequest.setQueryString("success");
|
||||
HttpServletRequest request = spy(mockRequest);
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(request, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNull();
|
||||
verify(request, never()).getParameter(anyString());
|
||||
verify(request, never()).getParameterValues(anyString());
|
||||
verify(request, never()).getParameterNames();
|
||||
verify(request, never()).getParameterMap();
|
||||
}
|
||||
|
||||
private static final class CustomSavedRequest implements SavedRequest {
|
||||
|
||||
private final SavedRequest delegate;
|
||||
|
||||
+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