Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 564a022ab6 | |||
| 779541b340 | |||
| 5b293d2116 | |||
| f026e29771 | |||
| 620e6e0c34 | |||
| 4ebfa2c804 | |||
| 3feb809b35 | |||
| 9b7a110704 | |||
| ce012a4661 | |||
| 96d1763fc4 | |||
| 8efbbb3eb5 | |||
| 612909ac4f | |||
| 28f98b3351 | |||
| ed96e2cddf | |||
| 7200f76ac1 | |||
| 094b71b329 | |||
| 491f0f647c | |||
| 1db8734101 | |||
| 4b44a2d924 | |||
| e8b9a35494 | |||
| 82e5f62079 | |||
| 30bc2634d7 | |||
| 13ca7ac4d4 | |||
| c4f061c63d | |||
| 2c6d053ceb | |||
| ff4745a461 | |||
| 38a7ffcc9b | |||
| 6f55fc60bb | |||
| d0491c07e9 | |||
| 486c5118d2 | |||
| d02ab50577 | |||
| 17d8293be1 | |||
| bb46a54270 | |||
| df239b6448 | |||
| a939f17890 | |||
| fe9bc26bdc | |||
| 7813a9ba26 | |||
| 7d58b9391a | |||
| dc3cb68c76 | |||
| 8555c100d7 | |||
| ae46531add | |||
| 0121b08a85 | |||
| 56292c9971 | |||
| 4fc938555e | |||
| e574415244 | |||
| c5da886310 | |||
| 3bef5fd3ed | |||
| b7a9a654f0 | |||
| e7201c48d1 | |||
| a642fdb004 | |||
| 991b398c55 | |||
| 40d61743b9 | |||
| 8895a66a2b | |||
| 80a5028f3f | |||
| c30bacac10 | |||
| a104dec30d | |||
| 488b6ea531 | |||
| ef04ea9d68 | |||
| b7be4c462c | |||
| aa8440e1ee | |||
| 6d3f1699c0 | |||
| bb72eed8d1 | |||
| 108075763b | |||
| 7da99acca7 | |||
| 0cf95dbf61 | |||
| 39c43159f4 | |||
| 67f262199a | |||
| 0a022a348c | |||
| afec90c8e8 | |||
| b29772269f | |||
| c7ccba66c7 | |||
| a6599f9874 | |||
| 9f581850a1 | |||
| 6e13927b48 | |||
| cead207c5b | |||
| 9e22d40264 | |||
| c54a191042 | |||
| 0da2e5a33e | |||
| 08b84502e3 | |||
| 6bacf21c76 | |||
| fe8f56e41c | |||
| f402679996 | |||
| 708af21576 | |||
| dbcff3f1af | |||
| 57b27cacdf | |||
| ed0cc16bb6 | |||
| e87fae40df | |||
| 32d25e8f23 | |||
| 0c63c7f611 | |||
| 9656f79afe | |||
| dc5e1eae1d | |||
| 81723fca9a | |||
| ac32095bc8 | |||
| 25390ab618 | |||
| 30206b6d0c | |||
| 3e22c1e8de | |||
| 13530d5eba | |||
| d67c11cfbd | |||
| a81c0459e2 | |||
| a387f401b7 | |||
| 9bd04ed0e0 | |||
| 312b758b3a | |||
| 68cec50bad | |||
| 04692d9ee8 | |||
| 1c6dc1aaac | |||
| 2279f9fd39 | |||
| df4411fb48 | |||
| e834543eed | |||
| 783f674704 | |||
| 933debebeb | |||
| b47420f8a2 | |||
| 7250abc185 | |||
| 9ac286e8ea | |||
| 06e58e4c34 | |||
| 79f1cf799d | |||
| bcc1cfc28a | |||
| aa8b691ee3 | |||
| 663f5cf76b | |||
| 8ccc03fdb6 | |||
| 5c88b95af5 | |||
| c1002ff745 | |||
| 73cb9862ad | |||
| f8e39336cb | |||
| a53cbb838b | |||
| 8287289bcb | |||
| 68b052218a | |||
| 62ede47d86 | |||
| 1eefd433b6 | |||
| 6b19728c54 | |||
| 35ad1f857e | |||
| 219faf29d4 | |||
| b6f3cb71e6 | |||
| 734dc98e50 | |||
| f4915890cc | |||
| ed0369ac71 | |||
| ce5570bb06 | |||
| 0c3bafb505 | |||
| 6a42d5c17b | |||
| 05ef215b88 | |||
| 9669747245 | |||
| b969179b5c | |||
| 3469bcb822 | |||
| 16b7d882c7 | |||
| b4083f1b9e | |||
| 1e093db1b6 | |||
| 8d5304f530 | |||
| e9a02bc6e9 | |||
| 6d37ca1808 | |||
| 5632469a90 | |||
| e61adcb0cd | |||
| f261242db1 | |||
| caa4093619 | |||
| 9244989b2e | |||
| 744b74f4c9 | |||
| 8bec14009e | |||
| 556ae316ba | |||
| 3a58666622 | |||
| 53d007f062 | |||
| d6eeafb10c |
+2
-1
@@ -1,4 +1,5 @@
|
||||
import io.spring.gradle.IncludeRepoTask
|
||||
import trang.RncToXsd
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
@@ -189,7 +190,7 @@ if (hasProperty('buildScan')) {
|
||||
|
||||
nohttp {
|
||||
source.exclude "buildSrc/build/**"
|
||||
|
||||
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
|
||||
}
|
||||
|
||||
tasks.register('cloneSamples', IncludeRepoTask) {
|
||||
|
||||
@@ -10,7 +10,6 @@ sourceCompatibility = 1.8
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/plugins-release/' }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -87,7 +86,7 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.0-M1'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.0'
|
||||
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'
|
||||
@@ -99,7 +98,7 @@ dependencies {
|
||||
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0'
|
||||
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.9.2')
|
||||
testImplementation platform('org.junit:junit-bom:5.9.3')
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
+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/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package io.spring.gradle.convention
|
||||
|
||||
import org.gradle.api.plugins.JavaPlugin
|
||||
import org.gradle.api.tasks.bundling.Zip
|
||||
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,8 +40,14 @@ public class SchemaZipPlugin implements Plugin<Project> {
|
||||
duplicatesStrategy 'exclude'
|
||||
from xsdFile.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
|
||||
@@ -113,12 +111,31 @@ dependencies {
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
}
|
||||
|
||||
rncToXsd {
|
||||
def versionlessXsd = project.tasks.create("versionlessXsd", CreateVersionlessXsdTask) {
|
||||
inputFiles.from(project.sourceSets.main.resources)
|
||||
versionlessXsdFile = project.layout.buildDirectory.file("versionlessXsd/spring-security.xsd")
|
||||
}
|
||||
|
||||
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.3"
|
||||
@@ -128,8 +145,6 @@ tasks.withType(KotlinCompile).configureEach {
|
||||
}
|
||||
}
|
||||
|
||||
build.dependsOn rncToXsd
|
||||
|
||||
compileTestJava {
|
||||
exclude "org/springframework/security/config/annotation/web/configurers/saml2/**", "org/springframework/security/config/http/Saml2*"
|
||||
}
|
||||
|
||||
+6
@@ -221,6 +221,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
|
||||
if (configs == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
removeFromConfigurersAddedInInitializing(clazz);
|
||||
return new ArrayList<>(configs);
|
||||
}
|
||||
|
||||
@@ -253,11 +254,16 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
|
||||
if (configs == null) {
|
||||
return null;
|
||||
}
|
||||
removeFromConfigurersAddedInInitializing(clazz);
|
||||
Assert.state(configs.size() == 1,
|
||||
() -> "Only one configurer expected for type " + clazz + ", but got " + configs);
|
||||
return (C) configs.get(0);
|
||||
}
|
||||
|
||||
private <C extends SecurityConfigurer<O, B>> void removeFromConfigurersAddedInInitializing(Class<C> clazz) {
|
||||
this.configurersAddedInInitializing.removeIf(clazz::isInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the {@link ObjectPostProcessor} to use.
|
||||
* @param objectPostProcessor the {@link ObjectPostProcessor} to use. Cannot be null
|
||||
|
||||
+4
-3
@@ -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.
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -47,7 +48,7 @@ final class Jsr250MethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor jsr250AuthorizationMethodInterceptor() {
|
||||
MethodInterceptor jsr250AuthorizationMethodInterceptor() {
|
||||
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
|
||||
.jsr250(this.jsr250AuthorizationManager);
|
||||
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
|
||||
|
||||
+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() };
|
||||
|
||||
+6
-4
@@ -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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
@@ -80,19 +82,19 @@ final class PrePostMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preFilterAuthorizationMethodInterceptor() {
|
||||
MethodInterceptor preFilterAuthorizationMethodInterceptor() {
|
||||
return this.preFilterAuthorizationMethodInterceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preAuthorizeAuthorizationMethodInterceptor() {
|
||||
MethodInterceptor preAuthorizeAuthorizationMethodInterceptor() {
|
||||
return this.preAuthorizeAuthorizationMethodInterceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postAuthorizeAuthorizationMethodInterceptor() {
|
||||
MethodInterceptor postAuthorizeAuthorizationMethodInterceptor() {
|
||||
return this.postAuthorizeAuthorizaitonMethodInterceptor;
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -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.
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -44,7 +45,7 @@ final class SecuredMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor securedAuthorizationMethodInterceptor() {
|
||||
MethodInterceptor securedAuthorizationMethodInterceptor() {
|
||||
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor.secured();
|
||||
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
|
||||
return interceptor;
|
||||
|
||||
+81
-12
@@ -18,9 +18,14 @@ package org.springframework.security.config.annotation.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletRegistration;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -36,6 +41,7 @@ import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
|
||||
/**
|
||||
@@ -297,14 +303,73 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
* @since 5.8
|
||||
*/
|
||||
public C requestMatchers(HttpMethod method, String... patterns) {
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
if (mvcPresent) {
|
||||
matchers.addAll(createMvcMatchers(method, patterns));
|
||||
if (!mvcPresent) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
else {
|
||||
matchers.addAll(RequestMatchers.antMatchers(method, patterns));
|
||||
if (!(this.context instanceof WebApplicationContext)) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
|
||||
WebApplicationContext context = (WebApplicationContext) this.context;
|
||||
ServletContext servletContext = context.getServletContext();
|
||||
if (servletContext == null) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
Map<String, ? extends ServletRegistration> registrations = mappableServletRegistrations(servletContext);
|
||||
if (registrations.isEmpty()) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
if (registrations.size() > 1) {
|
||||
String errorMessage = computeErrorMessage(registrations.values());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
}
|
||||
return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0]));
|
||||
}
|
||||
|
||||
private Map<String, ? extends ServletRegistration> mappableServletRegistrations(ServletContext servletContext) {
|
||||
Map<String, ServletRegistration> mappable = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, ? extends ServletRegistration> entry : servletContext.getServletRegistrations()
|
||||
.entrySet()) {
|
||||
if (!entry.getValue().getMappings().isEmpty()) {
|
||||
mappable.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return mappable;
|
||||
}
|
||||
|
||||
private boolean hasDispatcherServlet(Map<String, ? extends ServletRegistration> registrations) {
|
||||
if (registrations == null) {
|
||||
return false;
|
||||
}
|
||||
Class<?> dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet",
|
||||
null);
|
||||
for (ServletRegistration registration : registrations.values()) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
if (dispatcherServlet.isAssignableFrom(clazz)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
|
||||
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
|
||||
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
|
||||
+ "This is because there is more than one mappable servlet in your servlet context: %s.\n\n"
|
||||
+ "For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.";
|
||||
Map<String, Collection<String>> mappings = new LinkedHashMap<>();
|
||||
for (ServletRegistration registration : registrations) {
|
||||
mappings.put(registration.getClassName(), registration.getMappings());
|
||||
}
|
||||
return String.format(template, mappings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,12 +445,7 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
* @return a {@link List} of {@link AntPathRequestMatcher} instances
|
||||
*/
|
||||
static List<RequestMatcher> antMatchers(HttpMethod httpMethod, String... antPatterns) {
|
||||
String method = (httpMethod != null) ? httpMethod.toString() : null;
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
for (String pattern : antPatterns) {
|
||||
matchers.add(new AntPathRequestMatcher(pattern, method));
|
||||
}
|
||||
return matchers;
|
||||
return Arrays.asList(antMatchersAsArray(httpMethod, antPatterns));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -399,6 +459,15 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
return antMatchers(null, antPatterns);
|
||||
}
|
||||
|
||||
static RequestMatcher[] antMatchersAsArray(HttpMethod httpMethod, String... antPatterns) {
|
||||
String method = (httpMethod != null) ? httpMethod.toString() : null;
|
||||
RequestMatcher[] matchers = new RequestMatcher[antPatterns.length];
|
||||
for (int index = 0; index < antPatterns.length; index++) {
|
||||
matchers[index] = new AntPathRequestMatcher(antPatterns[index], method);
|
||||
}
|
||||
return matchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link List} of {@link RegexRequestMatcher} instances.
|
||||
* @param httpMethod the {@link HttpMethod} to use or {@code null} for any
|
||||
|
||||
+2
-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.
|
||||
@@ -103,7 +103,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public WebSecurityCustomizer webSecurityCustomizer(WebSecurity web) {
|
||||
* public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
* return (web) -> web.ignoring().antMatchers("/resources/**");
|
||||
* }
|
||||
* </pre> See the <a href=
|
||||
|
||||
+8
-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.
|
||||
@@ -35,6 +35,7 @@ import org.springframework.security.web.authentication.rememberme.PersistentToke
|
||||
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -288,6 +289,12 @@ public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
if (this.authenticationSuccessHandler != null) {
|
||||
rememberMeFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
}
|
||||
SecurityContextConfigurer<?> securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
|
||||
if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
|
||||
SecurityContextRepository securityContextRepository = securityContextConfigurer
|
||||
.getSecurityContextRepository();
|
||||
rememberMeFilter.setSecurityContextRepository(securityContextRepository);
|
||||
}
|
||||
rememberMeFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
rememberMeFilter = postProcess(rememberMeFilter);
|
||||
http.addFilter(rememberMeFilter);
|
||||
|
||||
+23
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -23,6 +24,8 @@ import org.springframework.security.web.server.util.matcher.OrServerWebExchangeM
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
@@ -62,7 +65,8 @@ public abstract class AbstractServerWebExchangeMatcherRegistry<T> {
|
||||
* {@link ServerWebExchangeMatcher}
|
||||
*/
|
||||
public T pathMatchers(HttpMethod method, String... antPatterns) {
|
||||
return matcher(ServerWebExchangeMatchers.pathMatchers(method, antPatterns));
|
||||
List<PathPattern> pathPatterns = parsePatterns(antPatterns);
|
||||
return matcher(ServerWebExchangeMatchers.pathMatchers(method, pathPatterns.toArray(new PathPattern[0])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +78,19 @@ public abstract class AbstractServerWebExchangeMatcherRegistry<T> {
|
||||
* {@link ServerWebExchangeMatcher}
|
||||
*/
|
||||
public T pathMatchers(String... antPatterns) {
|
||||
return matcher(ServerWebExchangeMatchers.pathMatchers(antPatterns));
|
||||
List<PathPattern> pathPatterns = parsePatterns(antPatterns);
|
||||
return matcher(ServerWebExchangeMatchers.pathMatchers(pathPatterns.toArray(new PathPattern[0])));
|
||||
}
|
||||
|
||||
private List<PathPattern> parsePatterns(String[] antPatterns) {
|
||||
PathPatternParser parser = getPathPatternParser();
|
||||
List<PathPattern> pathPatterns = new ArrayList<>(antPatterns.length);
|
||||
for (String pattern : antPatterns) {
|
||||
pattern = parser.initFullPathPattern(pattern);
|
||||
PathPattern pathPattern = parser.parse(pattern);
|
||||
pathPatterns.add(pathPattern);
|
||||
}
|
||||
return pathPatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,6 +112,10 @@ public abstract class AbstractServerWebExchangeMatcherRegistry<T> {
|
||||
*/
|
||||
protected abstract T registerMatcher(ServerWebExchangeMatcher matcher);
|
||||
|
||||
protected PathPatternParser getPathPatternParser() {
|
||||
return PathPatternParser.defaultInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a {@link ServerWebExchangeMatcher} instances
|
||||
* @param matcher the {@link ServerWebExchangeMatcher} instance
|
||||
|
||||
+35
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -190,9 +190,11 @@ import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.reactive.CorsProcessor;
|
||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but
|
||||
@@ -1562,6 +1564,18 @@ public class ServerHttpSecurity {
|
||||
return null;
|
||||
}
|
||||
|
||||
private <T> T getBeanOrNull(String beanName, Class<T> requiredClass) {
|
||||
if (this.context == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return this.context.getBean(beanName, requiredClass);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> String[] getBeanNamesForTypeOrEmpty(Class<T> beanClass) {
|
||||
if (this.context == null) {
|
||||
return new String[0];
|
||||
@@ -1582,6 +1596,8 @@ public class ServerHttpSecurity {
|
||||
*/
|
||||
public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegistry<AuthorizeExchangeSpec.Access> {
|
||||
|
||||
private static final String REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME = "requestMappingHandlerMapping";
|
||||
|
||||
private DelegatingReactiveAuthorizationManager.Builder managerBldr = DelegatingReactiveAuthorizationManager
|
||||
.builder();
|
||||
|
||||
@@ -1589,6 +1605,8 @@ public class ServerHttpSecurity {
|
||||
|
||||
private boolean anyExchangeRegistered;
|
||||
|
||||
private PathPatternParser pathPatternParser;
|
||||
|
||||
/**
|
||||
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
|
||||
* @return the {@link ServerHttpSecurity} to continue configuring
|
||||
@@ -1608,6 +1626,22 @@ public class ServerHttpSecurity {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PathPatternParser getPathPatternParser() {
|
||||
if (this.pathPatternParser != null) {
|
||||
return this.pathPatternParser;
|
||||
}
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping = getBeanOrNull(
|
||||
REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME, RequestMappingHandlerMapping.class);
|
||||
if (requestMappingHandlerMapping != null) {
|
||||
this.pathPatternParser = requestMappingHandlerMapping.getPathPatternParser();
|
||||
}
|
||||
if (this.pathPatternParser == null) {
|
||||
this.pathPatternParser = PathPatternParser.defaultInstance;
|
||||
}
|
||||
return this.pathPatternParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Access registerMatcher(ServerWebExchangeMatcher matcher) {
|
||||
Assert.state(!this.anyExchangeRegistered, () -> "Cannot register " + matcher
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.Servlet;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import javax.servlet.ServletSecurityElement;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
public class MockServletContext extends org.springframework.mock.web.MockServletContext {
|
||||
|
||||
private final Map<String, ServletRegistration> registrations = new LinkedHashMap<>();
|
||||
|
||||
public static MockServletContext mvc() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
|
||||
return servletContext;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ServletRegistration.Dynamic addServlet(@NonNull String servletName, Class<? extends Servlet> clazz) {
|
||||
ServletRegistration.Dynamic dynamic = new MockServletRegistration(servletName, clazz);
|
||||
this.registrations.put(servletName, dynamic);
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, ? extends ServletRegistration> getServletRegistrations() {
|
||||
return this.registrations;
|
||||
}
|
||||
|
||||
private static class MockServletRegistration implements ServletRegistration.Dynamic {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Class<?> clazz;
|
||||
|
||||
private final Set<String> mappings = new LinkedHashSet<>();
|
||||
|
||||
MockServletRegistration(String name, Class<?> clazz) {
|
||||
this.name = name;
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoadOnStartup(int loadOnStartup) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> setServletSecurity(ServletSecurityElement constraint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMultipartConfig(MultipartConfigElement multipartConfig) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRunAsRole(String roleName) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsyncSupported(boolean isAsyncSupported) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> addMapping(String... urlPatterns) {
|
||||
this.mappings.addAll(Arrays.asList(urlPatterns));
|
||||
return this.mappings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getMappings() {
|
||||
return this.mappings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRunAsRole() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClassName() {
|
||||
return this.clazz.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setInitParameter(String name, String value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInitParameter(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> setInitParameters(Map<String, String> initParameters) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getInitParameters() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
@@ -64,6 +65,8 @@ import org.springframework.security.test.context.support.WithSecurityContextTest
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -418,6 +421,17 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse();
|
||||
}
|
||||
|
||||
// gh-13572
|
||||
@Test
|
||||
public void configureWhenBeanOverridingDisallowedThenWorks() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, BusinessServiceConfig.class)
|
||||
.postProcessor(disallowBeanOverriding()).autowire();
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
static class MethodSecurityServiceConfig {
|
||||
|
||||
|
||||
+45
@@ -30,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
@@ -83,6 +84,24 @@ public class AbstractConfiguredSecurityBuilderTests {
|
||||
verify(DelegateSecurityConfigurer.CONFIGURER).configure(this.builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenConfigurerAppliesAndRemoveAnotherConfigurerThenNotConfigured() throws Exception {
|
||||
ApplyAndRemoveSecurityConfigurer.CONFIGURER = mock(SecurityConfigurer.class);
|
||||
this.builder.apply(new ApplyAndRemoveSecurityConfigurer());
|
||||
this.builder.build();
|
||||
verify(ApplyAndRemoveSecurityConfigurer.CONFIGURER, never()).init(this.builder);
|
||||
verify(ApplyAndRemoveSecurityConfigurer.CONFIGURER, never()).configure(this.builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenConfigurerAppliesAndRemoveAnotherConfigurersThenNotConfigured() throws Exception {
|
||||
ApplyAndRemoveAllSecurityConfigurer.CONFIGURER = mock(SecurityConfigurer.class);
|
||||
this.builder.apply(new ApplyAndRemoveAllSecurityConfigurer());
|
||||
this.builder.build();
|
||||
verify(ApplyAndRemoveAllSecurityConfigurer.CONFIGURER, never()).init(this.builder);
|
||||
verify(ApplyAndRemoveAllSecurityConfigurer.CONFIGURER, never()).configure(this.builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurerWhenMultipleConfigurersThenThrowIllegalStateException() throws Exception {
|
||||
TestConfiguredSecurityBuilder builder = new TestConfiguredSecurityBuilder(mock(ObjectPostProcessor.class),
|
||||
@@ -130,6 +149,32 @@ public class AbstractConfiguredSecurityBuilderTests {
|
||||
assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2);
|
||||
}
|
||||
|
||||
private static class ApplyAndRemoveSecurityConfigurer
|
||||
extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
|
||||
|
||||
private static SecurityConfigurer<Object, TestConfiguredSecurityBuilder> CONFIGURER;
|
||||
|
||||
@Override
|
||||
public void init(TestConfiguredSecurityBuilder builder) throws Exception {
|
||||
builder.apply(CONFIGURER);
|
||||
builder.removeConfigurer(CONFIGURER.getClass());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ApplyAndRemoveAllSecurityConfigurer
|
||||
extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
|
||||
|
||||
private static SecurityConfigurer<Object, TestConfiguredSecurityBuilder> CONFIGURER;
|
||||
|
||||
@Override
|
||||
public void init(TestConfiguredSecurityBuilder builder) throws Exception {
|
||||
builder.apply(CONFIGURER);
|
||||
builder.removeConfigurers(CONFIGURER.getClass());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DelegateSecurityConfigurer
|
||||
extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
|
||||
|
||||
|
||||
+50
-4
@@ -21,6 +21,7 @@ import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.Servlet;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -28,12 +29,15 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -56,12 +60,16 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
private TestRequestMatcherRegistry matcherRegistry;
|
||||
|
||||
private WebApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
public void setUp() throws Exception {
|
||||
this.matcherRegistry = new TestRequestMatcherRegistry();
|
||||
ApplicationContext context = mock(ApplicationContext.class);
|
||||
given(context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR);
|
||||
this.matcherRegistry.setApplicationContext(context);
|
||||
this.context = mock(WebApplicationContext.class);
|
||||
given(this.context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR);
|
||||
given(this.context.getServletContext()).willReturn(MockServletContext.mvc());
|
||||
this.matcherRegistry.setApplicationContext(this.context);
|
||||
mockMvcPresentClasspath(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,6 +192,44 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
"Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenNoDispatcherServletThenAntPathRequestMatcherType() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenAmbiguousServletsThenException() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/**");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenUnmappableServletsThenSkips() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
private void mockMvcIntrospector(boolean isPresent) {
|
||||
ApplicationContext context = this.matcherRegistry.getApplicationContext();
|
||||
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
|
||||
|
||||
+39
@@ -21,6 +21,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
@@ -46,6 +47,7 @@ import org.springframework.security.authentication.event.AuthenticationSuccessEv
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -56,6 +58,8 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
|
||||
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
@@ -323,6 +327,16 @@ public class HttpSecurityConfigurationTests {
|
||||
.resolveMediaTypes(any(NativeWebRequest.class));
|
||||
}
|
||||
|
||||
// gh-13203
|
||||
@Test
|
||||
public void disableConfigurerWhenAppliedByAnotherConfigurerThenNotApplied() {
|
||||
this.spring.register(ApplyCustomDslConfig.class).autowire();
|
||||
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
|
||||
List<Filter> filters = filterChain.getFilters();
|
||||
assertThat(filters).doesNotHaveAnyElementsOfTypes(DefaultLoginPageGeneratingFilter.class,
|
||||
DefaultLogoutPageGeneratingFilter.class);
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class NameController {
|
||||
|
||||
@@ -525,6 +539,31 @@ public class HttpSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class ApplyCustomDslConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.apply(CustomDsl.customDsl());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomDsl extends AbstractHttpConfigurer<CustomDsl, HttpSecurity> {
|
||||
|
||||
@Override
|
||||
public void init(HttpSecurity http) throws Exception {
|
||||
http.formLogin(FormLoginConfigurer::disable);
|
||||
}
|
||||
|
||||
static CustomDsl customDsl() {
|
||||
return new CustomDsl();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DefaultConfigurer extends AbstractHttpConfigurer<DefaultConfigurer, HttpSecurity> {
|
||||
|
||||
boolean init;
|
||||
|
||||
+2
-2
@@ -29,10 +29,10 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -75,7 +75,7 @@ public class AuthorizeRequestsTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.servletContext = spy(new MockServletContext());
|
||||
this.servletContext = spy(MockServletContext.mvc());
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ import org.springframework.core.annotation.Order;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -233,7 +233,7 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletContext(MockServletContext.mvc());
|
||||
this.context.refresh();
|
||||
this.context.getAutowireCapableBeanFactory().autowireBean(this);
|
||||
}
|
||||
|
||||
+79
-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.
|
||||
@@ -19,6 +19,8 @@ package org.springframework.security.config.annotation.web.configurers;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,6 +40,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
@@ -48,6 +51,9 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
|
||||
import org.springframework.security.web.context.HttpRequestResponseHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -60,6 +66,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
@@ -303,6 +310,24 @@ public class RememberMeConfigurerTests {
|
||||
this.mvc.perform(requestWithRememberme).andExpect(remembermeAuthentication);
|
||||
}
|
||||
|
||||
// gh-13104
|
||||
@Test
|
||||
public void getWhenCustomSecurityContextRepositoryThenUses() throws Exception {
|
||||
this.spring.register(SecurityContextRepositoryConfig.class).autowire();
|
||||
SecurityContextRepository repository = this.spring.getContext().getBean(SecurityContextRepository.class);
|
||||
MvcResult mvcResult = this.mvc.perform(post("/login").with(csrf()).param("username", "user")
|
||||
.param("password", "password").param("remember-me", "true")).andReturn();
|
||||
Cookie rememberMeCookie = mvcResult.getResponse().getCookie("remember-me");
|
||||
reset(repository);
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder request = get("/abc").cookie(rememberMeCookie);
|
||||
SecurityMockMvcResultMatchers.AuthenticatedMatcher remembermeAuthentication = authenticated()
|
||||
.withAuthentication((auth) -> assertThat(auth).isInstanceOf(RememberMeAuthenticationToken.class));
|
||||
// @formatter:on
|
||||
this.mvc.perform(request).andExpect(remembermeAuthentication);
|
||||
verify(repository).saveContext(any(), any(), any());
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class NullUserDetailsConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@@ -582,4 +607,57 @@ public class RememberMeConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class SecurityContextRepositoryConfig {
|
||||
|
||||
private SecurityContextRepository repository = spy(new SpySecurityContextRepository());
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.securityContext((context) -> context
|
||||
.requireExplicitSave(true)
|
||||
.securityContextRepository(this.repository)
|
||||
)
|
||||
.formLogin(withDefaults())
|
||||
.rememberMe(withDefaults());
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityContextRepository securityContextRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
|
||||
}
|
||||
|
||||
private static class SpySecurityContextRepository implements SecurityContextRepository {
|
||||
|
||||
SecurityContextRepository delegate = new HttpSessionSecurityContextRepository();
|
||||
|
||||
@Override
|
||||
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
|
||||
return this.delegate.loadContext(requestResponseHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
|
||||
this.delegate.saveContext(context, request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsContext(HttpServletRequest request) {
|
||||
return this.delegate.containsContext(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -167,7 +167,7 @@ public class UrlAuthorizationConfigurerTests {
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletContext(MockServletContext.mvc());
|
||||
this.context.refresh();
|
||||
this.context.getAutowireCapableBeanFactory().autowireBean(this);
|
||||
}
|
||||
|
||||
+28
@@ -318,6 +318,34 @@ public class Saml2LogoutConfigurerTests {
|
||||
verify(getBean(LogoutHandler.class)).logout(any(), any(), any());
|
||||
}
|
||||
|
||||
// gh-12346
|
||||
@Test
|
||||
public void saml2LogoutRequestWhenLowercaseEncodingAndDifferentQueryParamOrderThenLogsOutAndSendsLogoutResponse()
|
||||
throws Exception {
|
||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
||||
String apLogoutRequest = "nZFNa4QwEIb/iuQeP6K7dYO6FKQg2B622x56G3WwgiY2E8v239fqCksPPfSWIXmfNw+THC9D73yi\r\n"
|
||||
+ "oU6rlAWuzxxUtW461abs5fzAY3bMEoKhF6Msdasne8KPCck6c1KRXK9SNhklNVBHUsGAJG0tn+8f\r\n"
|
||||
+ "SylcX45GW13rnjn5HOwU2KXt3dqRpOeZ0cULDGOPrjat1y8t3gL2zFrGnCJPWXkKcR8KCHY8xmrP\r\n"
|
||||
+ "Iz868OpOVLwO4wohggagmd8STVgosqBsyoQvBPd3XITnIJaRL8PYjcThjTmvm/f8SXa1lEvY3Nr9\r\n"
|
||||
+ "LQdEaH6EWAYjR2U7+8W7JvFucRv8aY4X+b/g03zaoCsmu46/FpN9Aw==";
|
||||
String apLogoutRequestRelayState = "d118dbd5-3853-4268-b3e5-c40fc033fa2f";
|
||||
String apLogoutRequestSignature = "VZ7rWa5u3hIX60fAQs/gBQZWDP2BAIlCMMrNrTHafoKKj0uXWnuITYLuL8NdsWmyQN0+fqWW4X05+BqiLpL80jHLmQR5RVqqL1EtVv1SpPUna938lgz2sOliuYmfQNj4Bmd+Z5G1K6QhbVrtfb7TQHURjUafzfRm8+jGz3dPjVBrn/rD/umfGoSn6RuWngugcMNL4U0A+JcEh1NSfSYNVz7y+MqlW1UhX2kF86rm97ERCrxay7Gh/bI2f3fJPJ1r+EyLjzrDUkqw5cva3rVlFgEQouMVu35lUJn7SFompW8oTxkI23oc/t+AGZqaBupNITNdjyGCBpfukZ69EZrj8g==";
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user",
|
||||
Collections.emptyMap());
|
||||
principal.setRelyingPartyRegistrationId("get");
|
||||
Saml2Authentication user = new Saml2Authentication(principal, "response",
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
MvcResult result = this.mvc
|
||||
.perform(get("/logout/saml2/slo").param("SAMLRequest", apLogoutRequest)
|
||||
.param("SigAlg", this.apLogoutRequestSigAlg).param("RelayState", apLogoutRequestRelayState)
|
||||
.param("Signature", apLogoutRequestSignature)
|
||||
.with(new SamlQueryStringRequestPostProcessor(true)).with(authentication(user)))
|
||||
.andExpect(status().isFound()).andReturn();
|
||||
String location = result.getResponse().getHeader("Location");
|
||||
assertThat(location).startsWith("https://ap.example.org/logout/saml2/response");
|
||||
verify(getBean(LogoutHandler.class)).logout(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saml2LogoutRequestWhenNoRegistrationThen400() throws Exception {
|
||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
||||
|
||||
+2
-2
@@ -150,8 +150,8 @@ public class XsdDocumentedTests {
|
||||
.getParentFile()
|
||||
.list((dir, name) -> name.endsWith(".xsd"));
|
||||
// @formatter:on
|
||||
assertThat(schemas.length).isEqualTo(20)
|
||||
.withFailMessage("the count is equal to 20, if not then schemaDocument needs updating");
|
||||
assertThat(schemas.length).isEqualTo(21)
|
||||
.withFailMessage("the count is equal to 21, if not then schemaDocument needs updating");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -28,8 +28,8 @@ import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
||||
import org.springframework.mock.web.MockServletConfig;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.BeanIds;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.util.InMemoryXmlWebApplicationContext;
|
||||
import org.springframework.test.context.web.GenericXmlWebContextLoader;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
@@ -129,7 +129,7 @@ public class SpringTestContext implements Closeable {
|
||||
|
||||
public ConfigurableWebApplicationContext getContext() {
|
||||
if (!this.context.isRunning()) {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletContext(MockServletContext.mvc());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
this.context.refresh();
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class SpringTestContext implements Closeable {
|
||||
}
|
||||
|
||||
public void autowire() {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletContext(MockServletContext.mvc());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
for (Consumer<ConfigurableWebApplicationContext> postProcessor : this.postProcessors) {
|
||||
postProcessor.accept(this.context);
|
||||
|
||||
+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);
|
||||
};
|
||||
}
|
||||
|
||||
+14
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.authorization.method;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
@@ -96,6 +97,10 @@ final class AuthorizationAnnotationUtils {
|
||||
Class<A> annotationType) {
|
||||
boolean alreadyFound = false;
|
||||
for (MergedAnnotation<Annotation> mergedAnnotation : mergedAnnotations) {
|
||||
if (isSynthetic(mergedAnnotation.getSource())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mergedAnnotation.getType() == annotationType) {
|
||||
if (alreadyFound) {
|
||||
return true;
|
||||
@@ -106,6 +111,14 @@ final class AuthorizationAnnotationUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isSynthetic(Object object) {
|
||||
if (object instanceof Executable) {
|
||||
return ((Executable) object).isSynthetic();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private AuthorizationAnnotationUtils() {
|
||||
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.authorization.method;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorizationAnnotationUtils}
|
||||
*/
|
||||
class AuthorizationAnnotationUtilsTests {
|
||||
|
||||
@Test // gh-13132
|
||||
void annotationsOnSyntheticMethodsShouldNotTriggerAnnotationConfigurationException() throws NoSuchMethodException {
|
||||
StringRepository proxy = (StringRepository) Proxy.newProxyInstance(
|
||||
Thread.currentThread().getContextClassLoader(), new Class[] { StringRepository.class },
|
||||
(p, m, args) -> null);
|
||||
Method method = proxy.getClass().getDeclaredMethod("findAll");
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
|
||||
}
|
||||
|
||||
private interface BaseRepository<T> {
|
||||
|
||||
Iterable<T> findAll();
|
||||
|
||||
}
|
||||
|
||||
private interface StringRepository extends BaseRepository<String> {
|
||||
|
||||
@Override
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
List<String> findAll();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+6
-6
@@ -8,11 +8,11 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.31")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.3")
|
||||
api platform("org.junit:junit-bom:5.9.2")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.35")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.4")
|
||||
api platform("org.junit:junit-bom:5.9.3")
|
||||
api platform("org.mockito:mockito-bom:4.8.1")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.11")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.15")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
|
||||
@@ -20,7 +20,7 @@ dependencies {
|
||||
api "ch.qos.logback:logback-classic:1.2.12"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.24.4"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.43.1"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.43.3"
|
||||
api "com.squareup.okhttp3:mockwebserver:3.14.9"
|
||||
api "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
api "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
@@ -55,7 +55,7 @@ dependencies {
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
api "org.hsqldb:hsqldb:2.7.1"
|
||||
api "org.hsqldb:hsqldb:2.7.2"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
api "org.openid4java:openid4java-nodeps:0.9.6"
|
||||
api "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
name: ROOT
|
||||
name: security
|
||||
version: true
|
||||
title: Documentation
|
||||
title: Spring Security
|
||||
nav:
|
||||
- modules/ROOT/nav.adoc
|
||||
ext:
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
**** xref:servlet/appendix/namespace/method-security.adoc[Method Security]
|
||||
**** xref:servlet/appendix/namespace/ldap.adoc[LDAP Security]
|
||||
**** xref:servlet/appendix/namespace/websocket.adoc[WebSocket Security]
|
||||
*** xref:servlet/appendix/proxy-server.adoc[Proxy Server Configuration]
|
||||
*** xref:servlet/appendix/faq.adoc[FAQ]
|
||||
* xref:reactive/index.adoc[Reactive Applications]
|
||||
** xref:reactive/getting-started.adoc[Getting Started]
|
||||
|
||||
@@ -67,26 +67,31 @@ Instead Spring Security introduces `DelegatingPasswordEncoder` which solves all
|
||||
You can easily construct an instance of `DelegatingPasswordEncoder` using `PasswordEncoderFactories`.
|
||||
|
||||
.Create Default DelegatingPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
PasswordEncoder passwordEncoder =
|
||||
PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Alternatively, you may create your own custom instance. For example:
|
||||
|
||||
.Create Custom DelegatingPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
String idForEncode = "bcrypt";
|
||||
@@ -105,7 +110,8 @@ PasswordEncoder passwordEncoder =
|
||||
new DelegatingPasswordEncoder(idForEncode, encoders);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val idForEncode = "bcrypt"
|
||||
@@ -122,7 +128,7 @@ encoders["sha256"] = StandardPasswordEncoder()
|
||||
|
||||
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[authentication-password-storage-dpe-format]]
|
||||
=== Password Storage Format
|
||||
@@ -130,12 +136,10 @@ val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, en
|
||||
The general format for a password is:
|
||||
|
||||
.DelegatingPasswordEncoder Storage Format
|
||||
====
|
||||
[source,text,attrs="-attributes"]
|
||||
----
|
||||
{id}encodedPassword
|
||||
----
|
||||
====
|
||||
|
||||
Such that `id` is an identifier used to look up which `PasswordEncoder` should be used and `encodedPassword` is the original encoded password for the selected `PasswordEncoder`.
|
||||
The `id` must be at the beginning of the password, start with `{` and end with `}`.
|
||||
@@ -144,7 +148,6 @@ For example, the following might be a list of passwords encoded using different
|
||||
All of the original passwords are "password".
|
||||
|
||||
.DelegatingPasswordEncoder Encoded Passwords Example
|
||||
====
|
||||
[source,text,attrs="-attributes"]
|
||||
----
|
||||
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG // <1>
|
||||
@@ -153,7 +156,6 @@ All of the original passwords are "password".
|
||||
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= // <4>
|
||||
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 // <5>
|
||||
----
|
||||
====
|
||||
|
||||
<1> The first password would have a `PasswordEncoder` id of `bcrypt` and encodedPassword of `$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG`.
|
||||
When matching it would delegate to `BCryptPasswordEncoder`
|
||||
@@ -182,12 +184,10 @@ In the `DelegatingPasswordEncoder` we constructed above, that means that the res
|
||||
The end result would look like:
|
||||
|
||||
.DelegatingPasswordEncoder Encode Example
|
||||
====
|
||||
[source,text,attrs="-attributes"]
|
||||
----
|
||||
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
----
|
||||
====
|
||||
|
||||
[[authentication-password-storage-dpe-matching]]
|
||||
=== Password Matching
|
||||
@@ -209,8 +209,10 @@ If you are putting together a demo or a sample, it is a bit cumbersome to take t
|
||||
There are convenience mechanisms to make this easier, but this is still not intended for production.
|
||||
|
||||
.withDefaultPasswordEncoder Example
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
User user = User.withDefaultPasswordEncoder()
|
||||
@@ -222,7 +224,8 @@ System.out.println(user.getPassword());
|
||||
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
val user = User.withDefaultPasswordEncoder()
|
||||
@@ -233,13 +236,15 @@ val user = User.withDefaultPasswordEncoder()
|
||||
println(user.password)
|
||||
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are creating multiple users, you can also reuse the builder.
|
||||
|
||||
.withDefaultPasswordEncoder Reusing the Builder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||||
@@ -255,7 +260,8 @@ User admin = users
|
||||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val users = User.withDefaultPasswordEncoder()
|
||||
@@ -270,7 +276,7 @@ val admin = users
|
||||
.roles("USER", "ADMIN")
|
||||
.build()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This does hash the password that is stored, but the passwords are still exposed in memory and in the compiled source code.
|
||||
Therefore, it is still not considered secure for a production environment.
|
||||
@@ -284,13 +290,11 @@ The easiest way to properly encode your password is to use the https://docs.spri
|
||||
For example, the following will encode the password of `password` for use with <<authentication-password-storage-dpe>>:
|
||||
|
||||
.Spring Boot CLI encodepassword Example
|
||||
====
|
||||
[source,attrs="-attributes"]
|
||||
----
|
||||
spring encodepassword password
|
||||
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
|
||||
----
|
||||
====
|
||||
|
||||
[[authentication-password-storage-dpe-troubleshoot]]
|
||||
=== Troubleshooting
|
||||
@@ -336,8 +340,10 @@ The default implementation of `BCryptPasswordEncoder` uses strength 10 as mentio
|
||||
tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password.
|
||||
|
||||
.BCryptPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with strength 16
|
||||
@@ -346,7 +352,8 @@ String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with strength 16
|
||||
@@ -354,7 +361,7 @@ val encoder = BCryptPasswordEncoder(16)
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[authentication-password-storage-argon2]]
|
||||
== Argon2PasswordEncoder
|
||||
@@ -366,8 +373,10 @@ Like other adaptive one-way functions, it should be tuned to take about 1 second
|
||||
The current implementation of the `Argon2PasswordEncoder` requires BouncyCastle.
|
||||
|
||||
.Argon2PasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -376,7 +385,8 @@ String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -384,7 +394,7 @@ val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[authentication-password-storage-pbkdf2]]
|
||||
== Pbkdf2PasswordEncoder
|
||||
@@ -395,8 +405,10 @@ Like other adaptive one-way functions, it should be tuned to take about 1 second
|
||||
This algorithm is a good choice when FIPS certification is required.
|
||||
|
||||
.Pbkdf2PasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -405,7 +417,8 @@ String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -413,7 +426,7 @@ val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[authentication-password-storage-scrypt]]
|
||||
== SCryptPasswordEncoder
|
||||
@@ -423,8 +436,10 @@ In order to defeat password cracking on custom hardware scrypt is a deliberately
|
||||
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
|
||||
|
||||
.SCryptPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -433,7 +448,8 @@ String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -441,7 +457,7 @@ val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[authentication-password-storage-other]]
|
||||
== Other PasswordEncoders
|
||||
@@ -466,8 +482,10 @@ You should instead migrate to using `DelegatingPasswordEncoder` to support secur
|
||||
====
|
||||
|
||||
.NoOpPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -476,14 +494,16 @@ public static PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="passwordEncoder"
|
||||
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -491,7 +511,7 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -509,36 +529,42 @@ You can configure Spring Security to provide this discovery endpoint.
|
||||
For example, if the change password endpoint in your application is `/change-password`, then you can configure Spring Security like so:
|
||||
|
||||
.Default Change Password Endpoint
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
http
|
||||
.passwordManagement(Customizer.withDefaults())
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<sec:password-management/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
passwordManagement { }
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Then, when a password manager navigates to `/.well-known/change-password` then Spring Security will redirect your endpoint, `/change-password`.
|
||||
|
||||
Or, if your endpoint is something other than `/change-password`, you can also specify that like so:
|
||||
|
||||
.Change Password Endpoint
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
http
|
||||
@@ -547,13 +573,15 @@ http
|
||||
)
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<sec:password-management change-password-page="/update-password"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
@@ -562,6 +590,6 @@ http {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
With the above configuration, when a password manager navigates to `/.well-known/change-password`, then Spring Security will redirect to `/update-password`.
|
||||
|
||||
@@ -25,7 +25,6 @@ Assume that your bank's website provides a form that allows transferring money f
|
||||
For example, the transfer form might look like:
|
||||
|
||||
.Transfer form
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form method="post"
|
||||
@@ -40,12 +39,10 @@ For example, the transfer form might look like:
|
||||
value="Transfer"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
The corresponding HTTP request might look like:
|
||||
|
||||
.Transfer HTTP request
|
||||
====
|
||||
[source]
|
||||
----
|
||||
POST /transfer HTTP/1.1
|
||||
@@ -55,13 +52,11 @@ Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
amount=100.00&routingNumber=1234&account=9876
|
||||
----
|
||||
====
|
||||
|
||||
Now pretend you authenticate to your bank's website and then, without logging out, visit an evil website.
|
||||
The evil website contains an HTML page with the following form:
|
||||
|
||||
.Evil transfer form
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form method="post"
|
||||
@@ -79,7 +74,6 @@ The evil website contains an HTML page with the following form:
|
||||
value="Win Money!"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
You like to win money, so you click on the submit button.
|
||||
In the process, you have unintentionally transferred $100 to a malicious user.
|
||||
@@ -134,7 +128,6 @@ Assume the actual CSRF token is required to be in an HTTP parameter named `_csrf
|
||||
Our application's transfer form would look like:
|
||||
|
||||
.Synchronizer Token Form
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form method="post"
|
||||
@@ -152,7 +145,6 @@ Our application's transfer form would look like:
|
||||
value="Transfer"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
The form now contains a hidden input with the value of the CSRF token.
|
||||
External sites cannot read the CSRF token since the same origin policy ensures the evil site cannot read the response.
|
||||
@@ -160,7 +152,6 @@ External sites cannot read the CSRF token since the same origin policy ensures t
|
||||
The corresponding HTTP request to transfer money would look like this:
|
||||
|
||||
.Synchronizer Token request
|
||||
====
|
||||
[source]
|
||||
----
|
||||
POST /transfer HTTP/1.1
|
||||
@@ -170,7 +161,6 @@ Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
You will notice that the HTTP request now contains the `_csrf` parameter with a secure random value.
|
||||
@@ -191,12 +181,10 @@ Spring Framework's https://docs.spring.io/spring-framework/docs/current/javadoc-
|
||||
An example, HTTP response header with the `SameSite` attribute might look like:
|
||||
|
||||
.SameSite HTTP response
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
|
||||
----
|
||||
====
|
||||
|
||||
Valid values for the `SameSite` attribute are:
|
||||
|
||||
@@ -245,7 +233,6 @@ However, you must be very careful as there are CSRF exploits that can impact JSO
|
||||
For example, a malicious user can create a http://blog.opensecurityresearch.com/2012/02/json-csrf-with-parameter-padding.html[CSRF with JSON using the following form]:
|
||||
|
||||
.CSRF with JSON form
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
|
||||
@@ -254,13 +241,11 @@ For example, a malicious user can create a http://blog.opensecurityresearch.com/
|
||||
value="Win Money!"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
This will produce the following JSON structure
|
||||
|
||||
.CSRF with JSON request
|
||||
====
|
||||
[source,javascript]
|
||||
----
|
||||
{ "amount": 100,
|
||||
@@ -269,13 +254,11 @@ This will produce the following JSON structure
|
||||
"ignore_me": "=test"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
If an application were not validating the Content-Type, then it would be exposed to this exploit.
|
||||
Depending on the setup, a Spring MVC application that validates the Content-Type could still be exploited by updating the URL suffix to end with `.json` as shown below:
|
||||
|
||||
.CSRF with JSON Spring MVC form
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
|
||||
@@ -284,7 +267,6 @@ Depending on the setup, a Spring MVC application that validates the Content-Type
|
||||
value="Win Money!"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
[[csrf-when-stateless]]
|
||||
=== CSRF and Stateless Browser Applications
|
||||
@@ -393,7 +375,6 @@ In some applications a form parameter can be used to override the HTTP method.
|
||||
For example, the form below could be used to treat the HTTP method as a `delete` rather than a `post`.
|
||||
|
||||
.CSRF Hidden HTTP Method Form
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form action="/process"
|
||||
@@ -404,7 +385,6 @@ For example, the form below could be used to treat the HTTP method as a `delete`
|
||||
value="delete"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
Overriding the HTTP method occurs in a filter.
|
||||
|
||||
@@ -24,7 +24,6 @@ Spring Security provides a default set of security related HTTP response headers
|
||||
The default for Spring Security is to include the following headers:
|
||||
|
||||
.Default Security HTTP Response Headers
|
||||
====
|
||||
[source,http]
|
||||
----
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
@@ -35,7 +34,6 @@ Strict-Transport-Security: max-age=31536000 ; includeSubDomains
|
||||
X-Frame-Options: DENY
|
||||
X-XSS-Protection: 1; mode=block
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: Strict-Transport-Security is only added on HTTPS requests
|
||||
|
||||
@@ -62,14 +60,12 @@ If a user authenticates to view sensitive information and then logs out, we don'
|
||||
The cache control headers that are sent by default are:
|
||||
|
||||
.Default Cache Control HTTP Response Headers
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
----
|
||||
====
|
||||
|
||||
In order to be secure by default, Spring Security adds these headers by default.
|
||||
However, if your application provides its own cache control headers Spring Security will back out of the way.
|
||||
@@ -102,12 +98,10 @@ A malicious user might create a http://webblaze.cs.berkeley.edu/papers/barth-cab
|
||||
Spring Security disables content sniffing by default by adding the following header to HTTP responses:
|
||||
|
||||
.nosniff HTTP Response Header
|
||||
====
|
||||
[source,http]
|
||||
----
|
||||
X-Content-Type-Options: nosniff
|
||||
----
|
||||
====
|
||||
|
||||
[[headers-hsts]]
|
||||
== HTTP Strict Transport Security (HSTS)
|
||||
@@ -137,12 +131,10 @@ For example, Spring Security's default behavior is to add the following header w
|
||||
|
||||
|
||||
.Strict Transport Security HTTP Response Header
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
|
||||
----
|
||||
====
|
||||
|
||||
The optional `includeSubDomains` directive instructs the browser that subdomains (e.g. secure.mybank.example.com) should also be treated as an HSTS domain.
|
||||
|
||||
@@ -247,12 +239,10 @@ A security policy contains a set of security policy directives, each responsible
|
||||
For example, a web application can declare that it expects to load scripts from specific, trusted sources, by including the following header in the response:
|
||||
|
||||
.Content Security Policy Example
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Content-Security-Policy: script-src https://trustedscripts.example.com
|
||||
----
|
||||
====
|
||||
|
||||
An attempt to load a script from another source other than what is declared in the `script-src` directive will be blocked by the user-agent.
|
||||
Additionally, if the https://www.w3.org/TR/CSP2/#directive-report-uri[report-uri] directive is declared in the security policy, then the violation will be reported by the user-agent to the declared URL.
|
||||
@@ -260,12 +250,10 @@ Additionally, if the https://www.w3.org/TR/CSP2/#directive-report-uri[report-uri
|
||||
For example, if a web application violates the declared security policy, the following response header will instruct the user-agent to send violation reports to the URL specified in the policy's `report-uri` directive.
|
||||
|
||||
.Content Security Policy with report-uri
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/
|
||||
----
|
||||
====
|
||||
|
||||
https://www.w3.org/TR/CSP2/#violation-reports[Violation reports] are standard JSON structures that can be captured either by the web application's own API or by a publicly hosted CSP violation reporting service, such as, https://report-uri.com/.
|
||||
|
||||
@@ -276,12 +264,10 @@ When a policy is deemed effective, it can be enforced by using the `Content-Secu
|
||||
Given the following response header, the policy declares that scripts may be loaded from one of two possible sources.
|
||||
|
||||
.Content Security Policy Report Only
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/
|
||||
----
|
||||
====
|
||||
|
||||
If the site violates this policy, by attempting to load a script from _evil.com_, the user-agent will send a violation report to the declared URL specified by the _report-uri_ directive, but still allow the violating resource to load nevertheless.
|
||||
|
||||
@@ -308,12 +294,10 @@ page the user was on.
|
||||
Spring Security's approach is to use https://www.w3.org/TR/referrer-policy/[Referrer Policy] header, which provides different https://www.w3.org/TR/referrer-policy/#referrer-policies[policies]:
|
||||
|
||||
.Referrer Policy Example
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Referrer-Policy: same-origin
|
||||
----
|
||||
====
|
||||
|
||||
The Referrer-Policy response header instructs the browser to let the destination knows the source where the user was previously.
|
||||
|
||||
@@ -328,12 +312,10 @@ Refer to the relevant sections to see how to configure both xref:servlet/exploit
|
||||
https://wicg.github.io/feature-policy/[Feature Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser.
|
||||
|
||||
.Feature Policy Example
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Feature-Policy: geolocation 'self'
|
||||
----
|
||||
====
|
||||
|
||||
With Feature Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site.
|
||||
These policies restrict what APIs the site can access or modify the browser's default behavior for certain features.
|
||||
@@ -350,12 +332,10 @@ Refer to the relevant sections to see how to configure both xref:servlet/exploit
|
||||
https://w3c.github.io/webappsec-permissions-policy/[Permissions Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser.
|
||||
|
||||
.Permissions Policy Example
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Permissions-Policy: geolocation=(self)
|
||||
----
|
||||
====
|
||||
|
||||
With Permissions Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site.
|
||||
These policies restrict what APIs the site can access or modify the browser's default behavior for certain features.
|
||||
|
||||
@@ -14,8 +14,10 @@ It wraps a delegate `Runnable` in order to initialize the `SecurityContextHolder
|
||||
It then invokes the delegate Runnable ensuring to clear the `SecurityContextHolder` afterwards.
|
||||
The `DelegatingSecurityContextRunnable` looks something like this:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public void run() {
|
||||
@@ -28,7 +30,8 @@ try {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
fun run() {
|
||||
@@ -40,7 +43,7 @@ fun run() {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
While very simple, it makes it seamless to transfer the SecurityContext from one Thread to another.
|
||||
This is important since, in most cases, the SecurityContextHolder acts on a per Thread basis.
|
||||
@@ -48,8 +51,10 @@ For example, you might have used Spring Security's xref:servlet/appendix/namespa
|
||||
You can now easily transfer the `SecurityContext` of the current `Thread` to the `Thread` that invokes the secured service.
|
||||
An example of how you might do this can be found below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Runnable originalRunnable = new Runnable() {
|
||||
@@ -65,7 +70,8 @@ DelegatingSecurityContextRunnable wrappedRunnable =
|
||||
new Thread(wrappedRunnable).start();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val originalRunnable = Runnable {
|
||||
@@ -76,7 +82,7 @@ val wrappedRunnable = DelegatingSecurityContextRunnable(originalRunnable, contex
|
||||
|
||||
Thread(wrappedRunnable).start()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The code above performs the following steps:
|
||||
|
||||
@@ -90,8 +96,10 @@ Since it is quite common to create a `DelegatingSecurityContextRunnable` with th
|
||||
The following code is the same as the code above:
|
||||
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Runnable originalRunnable = new Runnable() {
|
||||
@@ -106,7 +114,8 @@ DelegatingSecurityContextRunnable wrappedRunnable =
|
||||
new Thread(wrappedRunnable).start();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val originalRunnable = Runnable {
|
||||
@@ -117,7 +126,7 @@ val wrappedRunnable = DelegatingSecurityContextRunnable(originalRunnable)
|
||||
|
||||
Thread(wrappedRunnable).start()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The code we have is simple to use, but it still requires knowledge that we are using Spring Security.
|
||||
In the next section we will take a look at how we can utilize `DelegatingSecurityContextExecutor` to hide the fact that we are using Spring Security.
|
||||
@@ -131,8 +140,10 @@ The design of `DelegatingSecurityContextExecutor` is very similar to that of `De
|
||||
You can see an example of how it might be used below:
|
||||
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
@@ -154,7 +165,8 @@ public void run() {
|
||||
executor.execute(originalRunnable);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val context: SecurityContext = SecurityContextHolder.createEmptyContext()
|
||||
@@ -171,7 +183,7 @@ val originalRunnable = Runnable {
|
||||
|
||||
executor.execute(originalRunnable)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The code performs the following steps:
|
||||
|
||||
@@ -185,8 +197,10 @@ In this instance, the same `SecurityContext` will be used for every Runnable sub
|
||||
This is nice if we are running background tasks that need to be run by a user with elevated privileges.
|
||||
* At this point you may be asking yourself "How does this shield my code of any knowledge of Spring Security?" Instead of creating the `SecurityContext` and the `DelegatingSecurityContextExecutor` in our own code, we can inject an already initialized instance of `DelegatingSecurityContextExecutor`.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Autowired
|
||||
@@ -202,7 +216,8 @@ executor.execute(originalRunnable);
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Autowired
|
||||
@@ -215,7 +230,7 @@ fun submitRunnable() {
|
||||
executor.execute(originalRunnable)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Now our code is unaware that the `SecurityContext` is being propagated to the `Thread`, then the `originalRunnable` is run, and then the `SecurityContextHolder` is cleared out.
|
||||
In this example, the same user is being used to run each thread.
|
||||
@@ -224,8 +239,10 @@ This can be done by removing the `SecurityContext` argument from our `Delegating
|
||||
For example:
|
||||
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
|
||||
@@ -233,13 +250,14 @@ DelegatingSecurityContextExecutor executor =
|
||||
new DelegatingSecurityContextExecutor(delegateExecutor);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val delegateExecutor = SimpleAsyncTaskExecutor()
|
||||
val executor = DelegatingSecurityContextExecutor(delegateExecutor)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Now anytime `executor.execute(Runnable)` is executed the `SecurityContext` is first obtained by the `SecurityContextHolder` and then that `SecurityContext` is used to create our `DelegatingSecurityContextRunnable`.
|
||||
This means that we are running our `Runnable` with the same user that was used to invoke the `executor.execute(Runnable)` code.
|
||||
|
||||
@@ -20,19 +20,22 @@ Encryptors are thread-safe.
|
||||
Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
|
||||
|
||||
.BytesEncryptor
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Encryptors.stronger("password", "salt");
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Encryptors.stronger("password", "salt")
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The "stronger" encryption method creates an encryptor using 256 bit AES encryption with
|
||||
Galois Counter Mode (GCM).
|
||||
@@ -46,19 +49,22 @@ The provided salt should be in hex-encoded String form, be random, and be at lea
|
||||
Such a salt may be generated using a KeyGenerator:
|
||||
|
||||
.Generating a key
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val salt = KeyGenerators.string().generateKey() // generates a random 8-byte salt that is then hex-encoded
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
|
||||
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
|
||||
@@ -70,19 +76,22 @@ For a more secure alternative, users should prefer `Encryptors.stronger`.
|
||||
Use the Encryptors.text factory method to construct a standard TextEncryptor:
|
||||
|
||||
.TextEncryptor
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Encryptors.text("password", "salt");
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Encryptors.text("password", "salt")
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
A TextEncryptor uses a standard BytesEncryptor to encrypt text data.
|
||||
Encrypted results are returned as hex-encoded strings for easy storage on the filesystem or in the database.
|
||||
@@ -90,19 +99,22 @@ Encrypted results are returned as hex-encoded strings for easy storage on the fi
|
||||
Use the Encryptors.queryableText factory method to construct a "queryable" TextEncryptor:
|
||||
|
||||
.Queryable TextEncryptor
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Encryptors.queryableText("password", "salt");
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Encryptors.queryableText("password", "salt")
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The difference between a queryable TextEncryptor and a standard TextEncryptor has to do with initialization vector (iv) handling.
|
||||
The iv used in a queryable TextEncryptor#encrypt operation is shared, or constant, and is not randomly generated.
|
||||
@@ -121,74 +133,86 @@ KeyGenerators are thread-safe.
|
||||
Use the KeyGenerators.secureRandom factory methods to generate a BytesKeyGenerator backed by a SecureRandom instance:
|
||||
|
||||
.BytesKeyGenerator
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
BytesKeyGenerator generator = KeyGenerators.secureRandom();
|
||||
byte[] key = generator.generateKey();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val generator = KeyGenerators.secureRandom()
|
||||
val key = generator.generateKey()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The default key length is 8 bytes.
|
||||
There is also a KeyGenerators.secureRandom variant that provides control over the key length:
|
||||
|
||||
.KeyGenerators.secureRandom
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
KeyGenerators.secureRandom(16);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
KeyGenerators.secureRandom(16)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Use the KeyGenerators.shared factory method to construct a BytesKeyGenerator that always returns the same key on every invocation:
|
||||
|
||||
.KeyGenerators.shared
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
KeyGenerators.shared(16);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
KeyGenerators.shared(16)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== StringKeyGenerator
|
||||
Use the KeyGenerators.string factory method to construct a 8-byte, SecureRandom KeyGenerator that hex-encodes each key as a String:
|
||||
|
||||
.StringKeyGenerator
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
KeyGenerators.string();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
KeyGenerators.string()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[spring-security-crypto-passwordencoders]]
|
||||
== Password Encoding
|
||||
@@ -219,8 +243,10 @@ The default value is 10.
|
||||
You can change this value in your deployed system without affecting existing passwords, as the value is also stored in the encoded hash.
|
||||
|
||||
.BCryptPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
@@ -230,7 +256,8 @@ String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
|
||||
@@ -239,15 +266,17 @@ val encoder = BCryptPasswordEncoder(16)
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The `Pbkdf2PasswordEncoder` implementation uses PBKDF2 algorithm to hash the passwords.
|
||||
In order to defeat password cracking PBKDF2 is a deliberately slow algorithm and should be tuned to take about .5 seconds to verify a password on your system.
|
||||
|
||||
|
||||
.Pbkdf2PasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -256,7 +285,8 @@ String result = encoder.encode("myPassword");
|
||||
assertTrue(encoder.matches("myPassword", result));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Create an encoder with all the defaults
|
||||
@@ -264,4 +294,4 @@ val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
|
||||
val result: String = encoder.encode("myPassword")
|
||||
assertTrue(encoder.matches("myPassword", result))
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -10,8 +10,10 @@ It is not only useful but necessary to include the user in the queries to suppor
|
||||
To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`.
|
||||
In Java Configuration, this would look like:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -20,7 +22,8 @@ public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -28,7 +31,7 @@ fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension {
|
||||
return SecurityEvaluationContextExtension()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In XML Configuration, this would look like:
|
||||
|
||||
@@ -43,8 +46,10 @@ In XML Configuration, this would look like:
|
||||
Now Spring Security can be used within your queries.
|
||||
For example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Repository
|
||||
@@ -54,7 +59,8 @@ public interface MessageRepository extends PagingAndSortingRepository<Message,Lo
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Repository
|
||||
@@ -63,7 +69,7 @@ interface MessageRepository : PagingAndSortingRepository<Message?, Long?> {
|
||||
fun findInbox(pageable: Pageable?): Page<Message?>?
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`.
|
||||
Note that this example assumes you have customized the principal to be an Object that has an id property.
|
||||
|
||||
@@ -6,8 +6,10 @@ This can improve the performance of serializing Spring Security related classes
|
||||
|
||||
To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]):
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
@@ -21,7 +23,8 @@ SecurityContext context = new SecurityContextImpl();
|
||||
String json = mapper.writeValueAsString(context);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val mapper = ObjectMapper()
|
||||
@@ -34,7 +37,7 @@ val context: SecurityContext = SecurityContextImpl()
|
||||
// ...
|
||||
val json: String = mapper.writeValueAsString(context)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
|
||||
@@ -30,7 +30,6 @@ Alternatively, you can manually add the starter, as the following example shows:
|
||||
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<dependencies>
|
||||
@@ -41,13 +40,11 @@ Alternatively, you can manually add the starter, as the following example shows:
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
Since Spring Boot provides a Maven BOM to manage dependency versions, you do not need to specify a version.
|
||||
If you wish to override the Spring Security version, you may do so by providing a Maven property, as the following example shows:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<properties>
|
||||
@@ -55,14 +52,12 @@ If you wish to override the Spring Security version, you may do so by providing
|
||||
<spring-security.version>{spring-security-version}</spring-security.version>
|
||||
</properties>
|
||||
----
|
||||
====
|
||||
|
||||
Since Spring Security makes breaking changes only in major releases, it is safe to use a newer version of Spring Security with Spring Boot.
|
||||
However, at times, you may need to update the version of Spring Framework as well.
|
||||
You can do so by adding a Maven property, as the following example shows:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<properties>
|
||||
@@ -70,7 +65,6 @@ You can do so by adding a Maven property, as the following example shows:
|
||||
<spring.version>{spring-core-version}</spring.version>
|
||||
</properties>
|
||||
----
|
||||
====
|
||||
|
||||
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate xref:modules.adoc#modules[Project Modules and Dependencies].
|
||||
|
||||
@@ -80,7 +74,6 @@ If you use additional features (such as LDAP, OpenID, and others), you need to a
|
||||
When you use Spring Security without Spring Boot, the preferred way is to use Spring Security's BOM to ensure a consistent version of Spring Security is used throughout the entire project. The following example shows how to do so:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml,ubs="verbatim,attributes"]
|
||||
----
|
||||
<dependencyManagement>
|
||||
@@ -96,12 +89,10 @@ When you use Spring Security without Spring Boot, the preferred way is to use Sp
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
----
|
||||
====
|
||||
|
||||
A minimal Spring Security Maven set of dependencies typically looks like the following:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<dependencies>
|
||||
@@ -116,7 +107,6 @@ A minimal Spring Security Maven set of dependencies typically looks like the fol
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
====
|
||||
|
||||
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate xref:modules.adoc#modules[Project Modules and Dependencies].
|
||||
|
||||
@@ -125,7 +115,6 @@ Many users are likely to run afoul of the fact that Spring Security's transitive
|
||||
The easiest way to resolve this is to use the `spring-framework-bom` within the `<dependencyManagement>` section of your `pom.xml` as the following example shows:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<dependencyManagement>
|
||||
@@ -141,7 +130,6 @@ The easiest way to resolve this is to use the `spring-framework-bom` within the
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
----
|
||||
====
|
||||
|
||||
The preceding example ensures that all the transitive dependencies of Spring Security use the Spring {spring-core-version} modules.
|
||||
|
||||
@@ -155,7 +143,6 @@ All GA releases (that is, versions ending in .RELEASE) are deployed to Maven Cen
|
||||
If you use a SNAPSHOT version, you need to ensure that you have the Spring Snapshot repository defined, as the following example shows:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
<repositories>
|
||||
@@ -167,12 +154,10 @@ If you use a SNAPSHOT version, you need to ensure that you have the Spring Snaps
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
|
||||
If you use a milestone or release candidate version, you need to ensure that you have the Spring Milestone repository defined, as the following example shows:
|
||||
|
||||
.pom.xml
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
<repositories>
|
||||
@@ -184,7 +169,6 @@ If you use a milestone or release candidate version, you need to ensure that you
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
====
|
||||
|
||||
[[getting-gradle]]
|
||||
== Gradle
|
||||
@@ -201,7 +185,6 @@ The simplest and preferred method to use the starter is to use https://docs.spri
|
||||
Alternatively, you can manually add the starter, as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
@@ -209,32 +192,27 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-security"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Since Spring Boot provides a Maven BOM to manage dependency versions, you need not specify a version.
|
||||
If you wish to override the Spring Security version, you may do so by providing a Gradle property, as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
ext['spring-security.version']='{spring-security-version}'
|
||||
----
|
||||
====
|
||||
|
||||
Since Spring Security makes breaking changes only in major releases, it is safe to use a newer version of Spring Security with Spring Boot.
|
||||
However, at times, you may need to update the version of Spring Framework as well.
|
||||
You can do so by adding a Gradle property, as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
ext['spring.version']='{spring-core-version}'
|
||||
----
|
||||
====
|
||||
|
||||
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate xref:modules.adoc#modules[Project Modules and Dependencies].
|
||||
|
||||
@@ -244,7 +222,6 @@ When you use Spring Security without Spring Boot, the preferred way is to use Sp
|
||||
You can do so by using the https://github.com/spring-gradle-plugins/dependency-management-plugin[Dependency Management Plugin], as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
@@ -258,12 +235,10 @@ dependencyManagement {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
A minimal Spring Security Maven set of dependencies typically looks like the following:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
@@ -272,7 +247,6 @@ dependencies {
|
||||
compile "org.springframework.security:spring-security-config"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate xref:modules.adoc#modules[Project Modules and Dependencies].
|
||||
|
||||
@@ -282,7 +256,6 @@ The easiest way to resolve this is to use the `spring-framework-bom` within your
|
||||
You can do so by using the https://github.com/spring-gradle-plugins/dependency-management-plugin[Dependency Management Plugin], as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
@@ -296,7 +269,6 @@ dependencyManagement {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The preceding example ensures that all the transitive dependencies of Spring Security use the Spring {spring-core-version} modules.
|
||||
|
||||
@@ -305,35 +277,29 @@ The preceding example ensures that all the transitive dependencies of Spring Sec
|
||||
All GA releases (that is, versions ending in .RELEASE) are deployed to Maven Central, so using the mavenCentral() repository is sufficient for GA releases. The following example shows how to do so:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
If you use a SNAPSHOT version, you need to ensure you have the Spring Snapshot repository defined, as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/snapshot' }
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
If you use a milestone or release candidate version, you need to ensure that you have the Spring Milestone repository defined, as the following example shows:
|
||||
|
||||
.build.gradle
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[[migration]]
|
||||
= Preparing for 6.0
|
||||
:spring-security-reference-base-url: https://docs.spring.io/spring-security/reference
|
||||
|
||||
The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0.
|
||||
Use 5.8 and the steps below to minimize changes when
|
||||
ifdef::spring-security-version[]
|
||||
xref:6.0.2@migration/index.adoc[updating to 6.0].
|
||||
{spring-security-reference-base-url}/6.0/migration/index.html[updating to 6.0].
|
||||
endif::[]
|
||||
ifndef::spring-security-version[]
|
||||
updating to 6.0.
|
||||
@@ -35,8 +36,10 @@ If you are xref:features/authentication/password-storage.adoc#authentication-pas
|
||||
|
||||
If you use the default constructor, you should begin by changing:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -45,7 +48,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -53,12 +57,14 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return Pbkdf2PasswordEncoder()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -67,7 +73,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -75,12 +82,14 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Or, if you have custom settings, change to the constructor that specifies all settings, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -90,7 +99,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -99,12 +109,14 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return current
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Change them to use the fully-specified constructor, like the following:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -114,7 +126,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -123,15 +136,17 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return current
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
==== Use `DelegatingPasswordEncoder`
|
||||
|
||||
Once you are not using the deprecated constructor, the next step is to prepare your code to upgrade to the latest standards by using `DelegatingPasswordEncoder`.
|
||||
The following code configures the delegating encoder to detect passwords that are using `current` and replace them with the latest:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -145,7 +160,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -158,7 +174,7 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return delegating
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Update `SCryptPasswordEncoder`
|
||||
|
||||
@@ -168,8 +184,10 @@ If you are xref:features/authentication/password-storage.adoc#authentication-pas
|
||||
|
||||
If you use the default constructor, you should begin by changing:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -178,7 +196,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -186,12 +205,14 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return SCryptPasswordEncoder()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -200,7 +221,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -208,15 +230,17 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
==== Use `DelegatingPasswordEncoder`
|
||||
|
||||
Once you are not using the deprecated constructor, the next step is to prepare your code to upgrade to the latest standards by using `DelegatingPasswordEncoder`.
|
||||
The following code configures the delegating encoder to detect passwords that are using `current` and replace them with the latest:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -230,7 +254,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -243,7 +268,7 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return delegating
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Update `Argon2PasswordEncoder`
|
||||
|
||||
@@ -253,8 +278,10 @@ If you are xref:features/authentication/password-storage.adoc#authentication-pas
|
||||
|
||||
If you use the default constructor, you should begin by changing:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -263,7 +290,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -271,12 +299,14 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return Argon2PasswordEncoder()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -285,7 +315,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -293,15 +324,17 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
==== Use `DelegatingPasswordEncoder`
|
||||
|
||||
Once you are not using the deprecated constructor, the next step is to prepare your code to upgrade to the latest standards by using `DelegatingPasswordEncoder`.
|
||||
The following code configures the delegating encoder to detect passwords that are using `current` and replace them with the latest:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -315,7 +348,8 @@ PasswordEncoder passwordEncoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -328,7 +362,7 @@ fun passwordEncoder(): PasswordEncoder {
|
||||
return delegating
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Stop using `Encryptors.queryableText`
|
||||
|
||||
@@ -339,8 +373,10 @@ To upgrade, you will either need to re-encrypt with a supported mechanism or sto
|
||||
|
||||
Consider the following pseudocode for reading each encrypted entry from a table, decrypting it, and then re-encrypting it using a supported mechanism:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
TextEncryptor deprecated = Encryptors.queryableText(password, salt);
|
||||
@@ -352,7 +388,7 @@ for (MyEntry entry : entries) {
|
||||
entryService.save(entry)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
<1> - The above uses the deprecated `queryableText` to convert the value to plaintext.
|
||||
<2> - Then, the value is re-encrypted with a supported Spring Security mechanism.
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ In Spring Security 5.8, the method `tokenFromMultipartDataEnabled` was deprecate
|
||||
To address the deprecation, the following code:
|
||||
|
||||
.Configure `tokenFromMultipartDataEnabled` with DSL
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -28,7 +30,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -41,13 +44,15 @@ open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChai
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
can be replaced with:
|
||||
|
||||
.Configure `tokenFromMultipartDataEnabled` with `ServerCsrfTokenRequestAttributeHandler`
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -63,7 +68,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -78,15 +84,17 @@ open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChai
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Protect against CSRF BREACH
|
||||
|
||||
You can opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration:
|
||||
|
||||
.`CsrfToken` BREACH Protection
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -102,7 +110,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -117,7 +126,7 @@ open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChai
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[reactive-csrf-breach-opt-out]]
|
||||
=== Opt-out Steps
|
||||
@@ -131,8 +140,10 @@ If you are using AngularJS and the https://angular.io/api/common/http/HttpClient
|
||||
In this case, you can configure Spring Security to validate the raw `CsrfToken` from the cookie while keeping CSRF BREACH protection of the response using a custom `ServerCsrfTokenRequestHandler` with delegation, like so:
|
||||
|
||||
.Configure `CsrfToken` BREACH Protection to validate raw tokens
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -163,7 +174,8 @@ WebFilter csrfCookieWebFilter() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -192,15 +204,17 @@ fun csrfCookieWebFilter(): WebFilter {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
==== I need to opt out of CSRF BREACH protection for another reason
|
||||
|
||||
If CSRF BREACH protection does not work for you for another reason, you can opt out using the following configuration:
|
||||
|
||||
.Opt out of `CsrfToken` BREACH protection
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -215,7 +229,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -229,7 +244,7 @@ open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChai
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Use `AuthorizationManager` for Method Security
|
||||
|
||||
@@ -245,35 +260,41 @@ In Spring Security 5.8, `useAuthorizationManager` was added to {security-api-url
|
||||
|
||||
To opt in, change `useAuthorizationManager` to `true` like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
changes to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[reactive-check-for-annotationconfigurationexceptions]]
|
||||
=== Check for ``AnnotationConfigurationException``s
|
||||
@@ -286,35 +307,41 @@ If after turning on `useAuthorizationManager` you see ``AnnotationConfigurationE
|
||||
|
||||
If you ran into trouble with `AuthorizationManager` for reactive method security, you can opt out by changing:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager = false)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager = false)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Propagate ``AuthenticationServiceException``s
|
||||
|
||||
@@ -327,21 +354,24 @@ To prepare for the 6.0 default, `httpBasic` and `oauth2ResourceServer` should be
|
||||
|
||||
For each, construct the appropriate authentication entry point for `httpBasic` and for `oauth2ResourceServer`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
ServerAuthenticationEntryPoint bearerEntryPoint = new BearerTokenServerAuthenticationEntryPoint();
|
||||
ServerAuthenticationEntryPoint basicEntryPoint = new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val bearerEntryPoint: ServerAuthenticationEntryPoint = BearerTokenServerAuthenticationEntryPoint()
|
||||
val basicEntryPoint: ServerAuthenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -350,8 +380,10 @@ If you use a custom `AuthenticationEntryPoint` for either or both mechanisms, us
|
||||
|
||||
Then, construct and configure a `ServerAuthenticationEntryPointFailureHandler` for each one:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
AuthenticationFailureHandler bearerFailureHandler = new ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint);
|
||||
@@ -360,7 +392,8 @@ AuthenticationFailureHandler basicFailureHandler = new ServerAuthenticationEntry
|
||||
basicFailureHandler.setRethrowAuthenticationServiceException(true)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val bearerFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint)
|
||||
@@ -368,12 +401,14 @@ bearerFailureHandler.setRethrowAuthenticationServiceException(true)
|
||||
val basicFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(basicEntryPoint)
|
||||
basicFailureHandler.setRethrowAuthenticationServiceException(true)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Finally, wire each authentication failure handler into the DSL, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
http
|
||||
@@ -381,7 +416,8 @@ http
|
||||
.oauth2ResourceServer((oauth2) -> oauth2.authenticationFailureHandler(bearerFailureHandler))
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
@@ -393,7 +429,7 @@ http {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[reactive-authenticationfailurehandler-opt-out]]
|
||||
=== Opt-out Steps
|
||||
@@ -408,8 +444,10 @@ In 6.0, `@Configuration` is removed from `@EnableWebFluxSecurity` and `@EnableRe
|
||||
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
|
||||
For example, `@EnableReactiveMethodSecurity` changes from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
@@ -417,8 +455,12 @@ public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
.Kotlin
|
||||
[tabs]
|
||||
======
|
||||
Kotlin::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
@@ -426,12 +468,14 @@ open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -440,8 +484,12 @@ public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
.Kotlin
|
||||
[tabs]
|
||||
======
|
||||
Kotlin::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -450,7 +498,7 @@ open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Address OAuth2 Client Deprecations
|
||||
|
||||
@@ -494,8 +542,10 @@ The following annotations had their `@Configuration` removed:
|
||||
|
||||
For example, if you are using `@EnableWebFluxSecurity`, you will need to change:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -503,12 +553,14 @@ public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -517,6 +569,6 @@ public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
And the same applies to every other annotation listed above.
|
||||
|
||||
@@ -17,8 +17,10 @@ See the xref:servlet/authentication/rememberme.adoc#_tokenbasedremembermeservice
|
||||
|
||||
[[servlet-opt-in-sha256-sha256-encoding]]
|
||||
.Use Spring Security 6 defaults for encoding, SHA-256 for encoding and MD5 for matching
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -46,7 +48,8 @@ public class SecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -61,15 +64,17 @@ public class SecurityConfig {
|
||||
<property name="encodingAlgorithm" value="SHA256"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
At some point, you will want to fully migrate to Spring Security 6 defaults. But how do you know when it is safe to do so?
|
||||
Let's suppose that you deployed your application using SHA-256 as the encoding algorithm (as you have done <<servlet-opt-in-sha256-sha256-encoding,here>>) on November 1st, if you have the value for the `tokenValiditySeconds` property set to N days (14 is the default), you can migrate to SHA-256 N days after November 1st (which is November 15th in this example).
|
||||
By that time, all the tokens generated with MD5 will have expired.
|
||||
|
||||
.Use Spring Security 6 defaults, SHA-256 for both encoding and matching
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -97,7 +102,8 @@ public class SecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -112,13 +118,15 @@ public class SecurityConfig {
|
||||
<property name="encodingAlgorithm" value="SHA256"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are having problems with the Spring Security 6 defaults, you can explicitly opt into 5.8 defaults using the following configuration:
|
||||
|
||||
.Use MD5 for both encoding and matching algorithms
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -146,7 +154,8 @@ public class SecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -161,7 +170,7 @@ public class SecurityConfig {
|
||||
<property name="encodingAlgorithm" value="MD5"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Propagate ``AuthenticationServiceException``s
|
||||
|
||||
@@ -172,8 +181,10 @@ Because ``AuthenticationServiceException``s represent a server-side error instea
|
||||
|
||||
To prepare for the 6.0 default, wire `AuthenticationFilter` instances with a `AuthenticationFailureHandler` that rethrows ``AuthenticationServiceException``s, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
AuthenticationFilter authenticationFilter = new AuthenticationFilter(...);
|
||||
@@ -182,7 +193,8 @@ handler.setRethrowAuthenticationServiceException(true);
|
||||
authenticationFilter.setAuthenticationFailureHandler(handler);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val authenticationFilter: AuthenticationFilter = new AuthenticationFilter(...)
|
||||
@@ -191,7 +203,8 @@ handler.setRethrowAuthenticationServiceException(true)
|
||||
authenticationFilter.setAuthenticationFailureHandler(handler)
|
||||
----
|
||||
|
||||
.Xml
|
||||
Xml::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<bean id="authenticationFilter" class="org.springframework.security.web.authentication.AuthenticationFilter">
|
||||
@@ -203,15 +216,17 @@ authenticationFilter.setAuthenticationFailureHandler(handler)
|
||||
<property name="rethrowAuthenticationServiceException" value="true"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-authenticationfailurehandler-opt-out]]
|
||||
=== Opt-out Steps
|
||||
|
||||
If rethrowing ``AuthenticationServiceException``s gives you trouble, you can set the value to false instead of taking the 6.0 default, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
AuthenticationFilter authenticationFilter = new AuthenticationFilter(...);
|
||||
@@ -220,7 +235,8 @@ handler.setRethrowAuthenticationServiceException(false);
|
||||
authenticationFilter.setAuthenticationFailureHandler(handler);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val authenticationFilter: AuthenticationFilter = new AuthenticationFilter(...)
|
||||
@@ -229,7 +245,8 @@ handler.setRethrowAuthenticationServiceException(false)
|
||||
authenticationFilter.setAuthenticationFailureHandler(handler)
|
||||
----
|
||||
|
||||
.Xml
|
||||
Xml::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<bean id="authenticationFilter" class="org.springframework.security.web.authentication.AuthenticationFilter">
|
||||
@@ -241,4 +258,4 @@ authenticationFilter.setAuthenticationFailureHandler(handler)
|
||||
<property name="rethrowAuthenticationServiceException" value="false"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,10 @@ In 6.0, `@Configuration` is removed from `@EnableWebSecurity`, `@EnableMethodSec
|
||||
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
|
||||
For example, `@EnableMethodSecurity` changes from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
@@ -19,8 +21,12 @@ public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
.Kotlin
|
||||
[tabs]
|
||||
======
|
||||
Kotlin::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
@@ -28,12 +34,14 @@ open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -42,8 +50,12 @@ public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
.Kotlin
|
||||
[tabs]
|
||||
======
|
||||
Kotlin::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -52,7 +64,7 @@ open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[use-new-requestmatchers]]
|
||||
== Use the new `requestMatchers` methods
|
||||
@@ -67,8 +79,10 @@ In summary, the new methods choose the `MvcRequestMatcher` implementation if you
|
||||
|
||||
To start using the new methods, you can replace the deprecated methods with the new ones. For example, the following application configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -88,12 +102,14 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
can be changed to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -113,13 +129,15 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you have Spring MVC in the classpath and are using the `mvcMatchers` methods, you can replace it with the new methods and Spring Security will choose the `MvcRequestMatcher` implementation for you.
|
||||
The following configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -139,12 +157,14 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
is equivalent to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -164,12 +184,14 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are customizing the `servletPath` property of the `MvcRequestMatcher`, you can now use the `MvcRequestMatcher.Builder` to create `MvcRequestMatcher` instances that share the same servlet path:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -190,12 +212,14 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The code above can be rewritten using the `MvcRequestMatcher.Builder` and the `requestMatchers` method:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -217,13 +241,15 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are having problem with the new `requestMatchers` methods, you can always switch back to the `RequestMatcher` implementation that you were using.
|
||||
For example, if you still want to use `AntPathRequestMatcher` and `RegexRequestMatcher` implementations, you can use the `requestMatchers` method that accepts a `RequestMatcher` instance:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||
@@ -247,14 +273,16 @@ public class SecurityConfig {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Note that the above sample uses static factory methods from {security-api-url}org/springframework/security/web/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] and {security-api-url}org/springframework/security/web/util/matcher/RegexRequestMatcher.html[`RegexRequestMatcher`] to improve readability.
|
||||
|
||||
If you are using the `WebSecurityCustomizer` interface, you can replace the deprecated `antMatchers` methods:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -262,12 +290,14 @@ public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with their `requestMatchers` counterparts:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -275,12 +305,14 @@ public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/ignore1", "/ignore2");
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The same way, if you are customizing the CSRF configuration to ignore some paths, you can replace the deprecated methods with the `requestMatchers` methods:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -292,12 +324,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
can be changed to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -309,7 +343,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[use-new-security-matchers]]
|
||||
== Use the new `securityMatchers` methods
|
||||
@@ -323,8 +357,10 @@ Another reason for adding the `securityMatchers` methods is to avoid confusion w
|
||||
|
||||
The following configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -338,12 +374,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
can be rewritten using the `securityMatchers` methods:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -357,12 +395,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are using a custom `RequestMatcher` in your `HttpSecurity` configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -380,12 +420,14 @@ public class MyCustomRequestMatcher implements RequestMatcher {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
you can do the same using `securityMatcher`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -403,12 +445,14 @@ public class MyCustomRequestMatcher implements RequestMatcher {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are combining multiple `RequestMatcher` implementations in your `HttpSecurity` configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -426,12 +470,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
you can change it by using `securityMatchers`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -448,12 +494,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you are having problems with the `securityMatchers` methods choosing the `RequestMatcher` implementation for you, you can always choose the `RequestMatcher` implementation yourself:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||
@@ -471,7 +519,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Stop Using `WebSecurityConfigurerAdapter`
|
||||
|
||||
@@ -481,8 +529,10 @@ Spring Security 5.4 introduced the capability to publish a `SecurityFilterChain`
|
||||
In 6.0, `WebSecurityConfigurerAdapter` is removed.
|
||||
To prepare for this change, you can replace constructs like:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -500,7 +550,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -519,12 +570,14 @@ open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -543,7 +596,8 @@ public class SecurityConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -562,15 +616,17 @@ open class SecurityConfiguration {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Publish a `WebSecurityCustomizer` Bean
|
||||
|
||||
Spring Security 5.4 https://github.com/spring-projects/spring-security/issues/8978[introduced `WebSecurityCustomizer`] to replace `configure(WebSecurity web)` in `WebSecurityConfigurerAdapter`.
|
||||
To prepare for its removal, you can replace code like the following:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -584,7 +640,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -596,12 +653,14 @@ open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -615,7 +674,8 @@ public class SecurityConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -628,19 +688,21 @@ open class SecurityConfiguration {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Publish an `AuthenticationManager` Bean
|
||||
|
||||
As part of `WebSecurityConfigurerAdapeter` removal, `configure(AuthenticationManagerBuilder)` is also removed.
|
||||
As part of `WebSecurityConfigurerAdapter` removal, `configure(AuthenticationManagerBuilder)` is also removed.
|
||||
Preparing for its removal will differ based on your reason for using it.
|
||||
|
||||
==== LDAP Authentication
|
||||
|
||||
If you are using `auth.ldapAuthentication()` for xref:servlet/authentication/passwords/ldap.adoc[LDAP authentication support], you can replace:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -659,7 +721,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -676,12 +739,14 @@ open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -705,7 +770,8 @@ public class SecurityConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -727,14 +793,16 @@ open class SecurityConfiguration {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
==== JDBC Authentication
|
||||
|
||||
If you are using `auth.jdbcAuthentication()` for xref:servlet/authentication/passwords/jdbc.adoc[JDBC Authentication support], you can replace:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -761,7 +829,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -786,12 +855,14 @@ open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -818,7 +889,8 @@ public class SecurityConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -844,14 +916,16 @@ open class SecurityConfiguration {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
==== In-Memory Authentication
|
||||
|
||||
If you are using `auth.inMemoryAuthentication()` for xref:servlet/authentication/passwords/in-memory.adoc[In-Memory Authentication support], you can replace:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -869,7 +943,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -885,12 +960,14 @@ open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -907,7 +984,8 @@ public class SecurityConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -923,7 +1001,7 @@ open class SecurityConfiguration {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Add `@Configuration` to `@Enable*` annotations
|
||||
|
||||
@@ -942,8 +1020,10 @@ The following annotations had their `@Configuration` removed:
|
||||
|
||||
For example, if you are using `@EnableWebSecurity`, you will need to change:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
@@ -951,12 +1031,14 @@ public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -965,10 +1047,10 @@ public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
And the same applies to every other annotation listed above.
|
||||
|
||||
==== Other Scenarios
|
||||
=== Other Scenarios
|
||||
|
||||
If you are using `AuthenticationManagerBuilder` for something more sophisticated, you can xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[publish your own `AuthenticationManager` `@Bean`] or wire an `AuthenticationManager` instance into the `HttpSecurity` DSL with {security-api-url}org/springframework/security/config/annotation/web/builders/HttpSecurity.html#authenticationManager(org.springframework.security.authentication.AuthenticationManager)[`HttpSecurity#authenticationManager`].
|
||||
|
||||
@@ -25,8 +25,10 @@ To opt into the new Spring Security 6 default, the following configuration can b
|
||||
|
||||
[[servlet-opt-in-defer-loading-csrf-token]]
|
||||
.Defer Loading `CsrfToken`
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -43,7 +45,8 @@ public SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -60,7 +63,8 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -71,7 +75,7 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"
|
||||
p:csrfRequestAttributeName="_csrf"/>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -92,8 +96,10 @@ In this case, you have several options for restoring the behavior your client-si
|
||||
One option is to add a `Filter` that eagerly renders the `CsrfToken` to the response regardless of which request is made first, like so:
|
||||
|
||||
.Add a `Filter` to return a cookie on the response
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -128,7 +134,8 @@ private static final class CsrfCookieFilter extends OncePerRequestFilter {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -159,7 +166,7 @@ class CsrfCookieFilter : OncePerRequestFilter() {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The option above does not require changes to the single-page application, but does cause the `CsrfToken` to be loaded on every request.
|
||||
If you do not wish to add a `Filter` to eagerly load tokens on every request, additional options are listed below.
|
||||
@@ -170,8 +177,10 @@ If you are using sessions, your application will benefit from deferred tokens.
|
||||
Instead of opting out, another option is to add a new `@RestController` with a `/csrf` endpoint, like so:
|
||||
|
||||
.Add a `/csrf` endpoint
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
@@ -185,7 +194,8 @@ public class CsrfController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
@@ -198,7 +208,7 @@ class CsrfController {
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -225,8 +235,10 @@ If you simply wish to opt out of deferred tokens altogether, that option is list
|
||||
If deferred tokens break your application for another reason, then you can explicitly opt into the 5.8 defaults using the following configuration:
|
||||
|
||||
.Explicit Configure `CsrfToken` with 5.8 Defaults
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -243,7 +255,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -260,7 +273,8 @@ open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -274,7 +288,7 @@ open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
</b:property>
|
||||
</b:bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -287,8 +301,10 @@ This causes the `CsrfToken` to be loaded on every request.
|
||||
If the steps for <<Defer Loading CsrfToken>> work for you, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration:
|
||||
|
||||
.`CsrfToken` BREACH Protection
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -305,7 +321,8 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -322,7 +339,8 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -333,7 +351,7 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"
|
||||
p:csrfRequestAttributeName="_csrf"/>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-csrf-breach-opt-out]]
|
||||
=== Opt-out Steps
|
||||
@@ -347,8 +365,10 @@ If you are using AngularJS and the https://angular.io/api/common/http/HttpClient
|
||||
In this case, you can configure Spring Security to validate the raw `CsrfToken` from the cookie while keeping CSRF BREACH protection of the response using a custom `CsrfTokenRequestHandler` with delegation, like so:
|
||||
|
||||
.Configure `CsrfToken` BREACH Protection to validate raw tokens
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -371,7 +391,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -393,7 +414,8 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -405,7 +427,7 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
|
||||
p:cookieHttpOnly="false"/>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This is the RECOMMENDED way to configure Spring Security to work with a client-side application that uses cookie values, because it continues to allow the response to return a randomized value for the CSRF token in case the application returns HTML or other responses that could be vulnerable to BREACH without your knowledge.
|
||||
|
||||
@@ -431,8 +453,10 @@ If CSRF BREACH protection does not work for you for another reason, you can opt
|
||||
If the steps for <<Protect against CSRF BREACH>> work for normal HTTP requests and you are using xref:servlet/integrations/websocket.adoc[WebSocket Security] support, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` with xref:servlet/integrations/websocket.adoc#websocket-sameorigin-csrf[Stomp headers].
|
||||
|
||||
.WebSocket Security BREACH Protection
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -441,7 +465,8 @@ ChannelInterceptor csrfChannelInterceptor() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -450,19 +475,22 @@ open fun csrfChannelInterceptor(): ChannelInterceptor {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="csrfChannelInterceptor"
|
||||
class="org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor"/>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If configuring CSRF BREACH protection for WebSocket Security gives you trouble, you can configure the 5.8 default using the following configuration:
|
||||
|
||||
.Configure WebSocket Security with 5.8 default
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -471,7 +499,8 @@ ChannelInterceptor csrfChannelInterceptor() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -480,10 +509,11 @@ open fun csrfChannelInterceptor(): ChannelInterceptor {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="csrfChannelInterceptor"
|
||||
class="org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor"/>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -20,8 +20,10 @@ If you are using authorization rules or expressions such as `hasRole("USER")` or
|
||||
To opt into the new Spring Security 6 defaults, the following configuration can be used.
|
||||
|
||||
.Configure oauth2Login() with 6.0 defaults
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -63,7 +65,8 @@ private GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -94,14 +97,15 @@ private fun grantedAuthoritiesMapper(): GrantedAuthoritiesMapper {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
<oauth2-login user-authorities-mapper-ref="userAuthoritiesMapper" ... />
|
||||
</http>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-oauth2-login-authorities-opt-out]]
|
||||
=== Opt-out Steps
|
||||
@@ -109,8 +113,10 @@ private fun grantedAuthoritiesMapper(): GrantedAuthoritiesMapper {
|
||||
If configuring the new authorities gives you trouble, you can opt out and explicitly use the 5.8 authority of `ROLE_USER` with the following configuration.
|
||||
|
||||
.Configure oauth2Login() with 5.8 defaults
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -152,7 +158,8 @@ private GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -183,14 +190,15 @@ private fun grantedAuthoritiesMapper(): GrantedAuthoritiesMapper {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
<oauth2-login user-authorities-mapper-ref="userAuthoritiesMapper" ... />
|
||||
</http>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Address OAuth2 Client Deprecations
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ As such, Spring Security 6 drops support for it, bumping up its OpenSAML baselin
|
||||
|
||||
To prepare for the upgrade, update your pom to depend on OpenSAML 4 instead of 3:
|
||||
|
||||
====
|
||||
.Maven
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,maven,role="primary"]
|
||||
----
|
||||
<dependencyManagement>
|
||||
@@ -32,7 +34,8 @@ To prepare for the upgrade, update your pom to depend on OpenSAML 4 instead of 3
|
||||
</dependencyManagement>
|
||||
----
|
||||
|
||||
.Gradle
|
||||
Gradle::
|
||||
+
|
||||
[source,gradle,role="secondary"]
|
||||
----
|
||||
dependencies {
|
||||
@@ -43,7 +46,7 @@ dependencies {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You must use at least OpenSAML 4.1.1 to update to Spring Security 6's SAML support.
|
||||
|
||||
@@ -57,8 +60,10 @@ As such, some adjustment will be required to make the challenge.
|
||||
|
||||
Consider the following representative usage of `OpenSamlAuthenticationProvider`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
OpenSamlAuthenticationProvider versionThree = new OpenSamlAuthenticationProvider();
|
||||
@@ -66,19 +71,22 @@ versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor);
|
||||
versionThree.setResponseTimeValidationSkew(myDuration);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val versionThree: OpenSamlAuthenticationProvider = OpenSamlAuthenticationProvider()
|
||||
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor)
|
||||
versionThree.setResponseTimeValidationSkew(myDuration)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This should change to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Converter<ResponseToken, Saml2Authentication> delegate = OpenSaml4AuthenticationProvider
|
||||
@@ -96,7 +104,8 @@ Converter<AssertionToken, Saml2ResponseValidationResult> validator = OpenSaml4Au
|
||||
versionFour.setAssertionValidator(validator);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val delegate = OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter()
|
||||
@@ -114,7 +123,7 @@ val validator = OpenSaml4AuthenticationProvider
|
||||
.createDefaultAssertionValidatorWithParameters({ p -> p.put(CLOCK_SKEW, myDuration) })
|
||||
versionFour.setAssertionValidator(validator)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Stop Using SAML 2.0 `Converter` constructors
|
||||
|
||||
@@ -136,8 +145,10 @@ Most applications need do nothing; however, if you use or configure `Saml2Authen
|
||||
|
||||
If you are calling `OpenSaml4AuthenticationReqeustFactory#setAuthenticationRequestContextConverter`, for example, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -167,12 +178,14 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory() {
|
||||
return factory;
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to ensure that ForceAuthn is set to `true`, you can instead do:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -182,7 +195,7 @@ Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyReg
|
||||
return resolver;
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Also, since `setAuthnRequestCustomizer` has direct access to the `HttpServletRequest`, there is no need for a `Saml2AuthenticationRequestContextResolver`.
|
||||
Simply use `setAuthnRequestCustomizer` to read directly from `HttpServletRequest` this information you need.
|
||||
@@ -191,8 +204,10 @@ Simply use `setAuthnRequestCustomizer` to read directly from `HttpServletRequest
|
||||
|
||||
Instead of doing:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -202,12 +217,14 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory() {
|
||||
return factory;
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
you can do:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -218,7 +235,7 @@ Saml2AuthenticationRequestResolver authenticationRequestResolver() {
|
||||
return resolver;
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -235,37 +252,43 @@ It also is valuable in that it more closely aligns with the design of `OAuth2Log
|
||||
Most applications do not construct this class directly since `Saml2WebSsoAuthenticationFilter` does.
|
||||
However, in the event that your application constructs one, please change from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
new Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(),
|
||||
registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials())
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(),
|
||||
registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials())
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
new Saml2AuthenticationToken(saml2Response, registration)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Saml2AuthenticationToken(saml2Response, registration)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Use `RelyingPartyRegistration` updated methods
|
||||
|
||||
@@ -275,8 +298,10 @@ As more capabilities were added to `RelyingPartyRegistration`, it became necessa
|
||||
The deprecated methods in `RelyingPartyRegstration` are removed.
|
||||
To prepare for that, consider the following representative usage of `RelyingPartyRegistration`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
String idpEntityId = registration.getRemoteIdpEntityId();
|
||||
@@ -288,7 +313,8 @@ List<Saml2X509Credential> verifying = registration.getCredentials().stream()
|
||||
.collect(Collectors.toList());
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val idpEntityId: String = registration.getRemoteIdpEntityId()
|
||||
@@ -298,12 +324,14 @@ val localEntityId: String = registration.getLocalEntityIdTemplate()
|
||||
val verifying: List<Saml2X509Credential> = registration.getCredentials()
|
||||
.filter(Saml2X509Credential::isSignatureVerficationCredential)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This should change to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId();
|
||||
@@ -313,7 +341,8 @@ String entityId = registration.getEntityId();
|
||||
List<Saml2X509Credential> verifying = registration.getAssertingPartyDetails().getVerificationX509Credentials();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val assertingPartyEntityId: String = registration.getAssertingPartyDetails().getEntityId()
|
||||
@@ -322,6 +351,6 @@ val singleSignOnServiceLocation: String = registration.getAssertingPartyDetails(
|
||||
val entityId: String = registration.getEntityId()
|
||||
val verifying: List<Saml2X509Credential> = registration.getAssertingPartyDetails().getVerificationX509Credentials()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
For a complete listing of all changed methods, please see {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[``RelyingPartyRegistration``'s JavaDoc].
|
||||
|
||||
@@ -28,8 +28,10 @@ In Spring Security 6, the default `SecurityContextRepository` is `DelegatingSecu
|
||||
To opt into the new Spring Security 6 default, the following configuration can be used.
|
||||
|
||||
.Configure SecurityContextRepository with 6.0 defaults
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -46,7 +48,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -64,7 +67,8 @@ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http security-context-repository-ref="contextRepository">
|
||||
@@ -80,7 +84,7 @@ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
@@ -103,8 +107,10 @@ If you have implemented `SecurityContextRepository` yourself and added an implem
|
||||
To get started implementing the new method, use the following example to provide a `DeferredSecurityContext`:
|
||||
|
||||
.Provide `DeferredSecurityContext`
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Override
|
||||
@@ -135,7 +141,8 @@ public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
override fun loadDeferredContext(request: HttpServletRequest): DeferredSecurityContext {
|
||||
@@ -159,7 +166,7 @@ override fun loadDeferredContext(request: HttpServletRequest): DeferredSecurityC
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[requestcache-query-optimization]]
|
||||
== Optimize Querying of `RequestCache`
|
||||
@@ -186,8 +193,10 @@ This means that there is no need to detect when `Authentication` is done and thu
|
||||
To opt into the new Spring Security 6 default, the following configuration can be used.
|
||||
|
||||
.Require Explicit `SessionAuthenticationStrategy` Invocation
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -201,7 +210,8 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -215,7 +225,8 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -223,13 +234,15 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
<session-management authentication-strategy-explicit-invocation="true"/>
|
||||
</http>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If this breaks your application, then you can explicitly opt into the 5.8 defaults using the following configuration:
|
||||
|
||||
.Explicit use Spring Security 5.8 defaults for `SessionAuthenticationStrategy`
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -243,7 +256,8 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -257,7 +271,8 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -265,4 +280,4 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
<session-management authentication-strategy-explicit-invocation="false"/>
|
||||
</http>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -21,7 +21,7 @@ Where a module depends on another Spring Security module, the non-optional depen
|
||||
|
||||
[[spring-security-core]]
|
||||
== Core -- `spring-security-core.jar`
|
||||
This module contains core authentication and access-contol classes and interfaces, remoting support, and basic provisioning APIs.
|
||||
This module contains core authentication and access-control classes and interfaces, remoting support, and basic provisioning APIs.
|
||||
It is required by any application that uses Spring Security.
|
||||
It supports standalone applications, remote clients, method (service layer) security, and JDBC user provisioning.
|
||||
It contains the following top-level packages:
|
||||
@@ -317,7 +317,7 @@ It requires OpenID4Java.
|
||||
This module contains support for testing with Spring Security.
|
||||
|
||||
[[spring-security-taglibs]]
|
||||
== Taglibs -- `spring-secuity-taglibs.jar`
|
||||
== Taglibs -- `spring-security-taglibs.jar`
|
||||
Provides Spring Security's JSP tag implementations.
|
||||
|
||||
.Taglib Dependencies
|
||||
|
||||
@@ -11,7 +11,10 @@ This will:
|
||||
Often, you will want to also invalidate the session on logout.
|
||||
To achieve this, you can add the `WebSessionServerLogoutHandler` to your logout configuration, like so:
|
||||
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -28,7 +31,8 @@ SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -47,3 +51,4 @@ fun http(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
Similar to xref:servlet/authentication/x509.adoc#servlet-x509[Servlet X.509 authentication], reactive x509 authentication filter allows extracting an authentication token from a certificate provided by a client.
|
||||
|
||||
Below is an example of a reactive x509 security configuration:
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -19,7 +21,8 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -32,14 +35,16 @@ fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In the configuration above, when neither `principalExtractor` nor `authenticationManager` is provided defaults will be used. The default principal extractor is `SubjectDnX509PrincipalExtractor` which extracts the CN (common name) field from a certificate provided by a client. The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager` which performs user account validation, checking that user account with a name extracted by `principalExtractor` exists and it is not locked, disabled, or expired.
|
||||
|
||||
The next example demonstrates how these defaults can be overridden.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -66,7 +71,8 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -88,7 +94,7 @@ fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In this example, a username is extracted from the OU field of a client certificate instead of CN, and account lookup using `ReactiveUserDetailsService` is not performed at all. Instead, if the provided certificate issued to an OU named "Trusted Org Unit", a request will be authenticated.
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ By default, Spring Security’s authorization will require all requests to be au
|
||||
The explicit configuration looks like:
|
||||
|
||||
.All Requests Require Authenticated User
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -22,7 +24,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -36,14 +39,16 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
We can configure Spring Security to have different rules by adding more rules in order of precedence.
|
||||
|
||||
.Multiple Authorize Requests Rules
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.hasRole;
|
||||
@@ -68,7 +73,8 @@ SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -88,7 +94,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
<1> There are multiple authorization rules specified.
|
||||
Each rule is considered in the order they were declared.
|
||||
|
||||
@@ -32,8 +32,10 @@ For earlier versions, please read about similar support with <<jc-enable-reactiv
|
||||
For example, the following would enable Spring Security's `@PreAuthorize` annotation:
|
||||
|
||||
.Method Security Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
|
||||
@@ -41,15 +43,17 @@ public class MethodSecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
|
||||
Spring Security's native annotation support defines a set of attributes for the method.
|
||||
These will be passed to the various method interceptors, like `AuthorizationManagerBeforeReactiveMethodInterceptor`, for it to make the actual decision:
|
||||
|
||||
.Method Security Annotation Usage
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public interface BankService {
|
||||
@@ -63,7 +67,7 @@ public interface BankService {
|
||||
Mono<Account> post(Account account, Double amount);
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In this case `hasRole` refers to the method found in `SecurityExpressionRoot` that gets invoked by the SpEL evaluation engine.
|
||||
|
||||
@@ -71,8 +75,10 @@ In this case `hasRole` refers to the method found in `SecurityExpressionRoot` th
|
||||
A bean like that might look something like this:
|
||||
|
||||
.Method Security Reactive Boolean Expression
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -80,20 +86,22 @@ public Function<Account, Mono<Boolean>> func() {
|
||||
return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Customizing Authorization
|
||||
|
||||
Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support.
|
||||
|
||||
[[jc-reactive-method-security-custom-granted-authority-defaults]]
|
||||
|
||||
[[jc-reactive-method-security-custom-granted-authority-defaults]]
|
||||
Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`.
|
||||
You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so:
|
||||
|
||||
.Custom MethodSecurityExpressionHandler
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -101,7 +109,7 @@ static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -124,8 +132,10 @@ If that authorization denies access, the value is not returned, and an `AccessDe
|
||||
To recreate what adding `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` does by default, you would publish the following configuration:
|
||||
|
||||
.Full Pre-post Method Security Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -160,15 +170,17 @@ class MethodSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Notice that Spring Security's method security is built using Spring AOP.
|
||||
So, interceptors are invoked based on the order specified.
|
||||
This can be customized by calling `setOrder` on the interceptor instances like so:
|
||||
|
||||
.Publish Custom Advisor
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -179,13 +191,15 @@ Advisor postFilterAuthorizationMethodInterceptor() {
|
||||
return interceptor;
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You may want to only support `@PreAuthorize` in your application, in which case you can do the following:
|
||||
|
||||
.Only @PreAuthorize Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -202,7 +216,7 @@ class MethodSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Or, you may have a custom before-method `ReactiveAuthorizationManager` that you want to add to the list.
|
||||
|
||||
@@ -211,9 +225,11 @@ In this case, you will need to tell Spring Security both the `ReactiveAuthorizat
|
||||
Thus, you can configure Spring Security to invoke your `ReactiveAuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so:
|
||||
|
||||
.Custom Before Advisor
|
||||
====
|
||||
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
|
||||
@@ -230,7 +246,7 @@ class MethodSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -243,8 +259,10 @@ After-method authorization is generally concerned with analysing the return valu
|
||||
For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
|
||||
|
||||
.@PostAuthorize example
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public interface BankService {
|
||||
@@ -254,7 +272,7 @@ public interface BankService {
|
||||
Mono<Account> readAccount(Long id);
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated.
|
||||
|
||||
@@ -262,8 +280,10 @@ For example, if you have your own custom annotation, you can configure it like s
|
||||
|
||||
|
||||
.Custom After Advisor
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
|
||||
@@ -278,7 +298,7 @@ class MethodSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
and it will be invoked after the `@PostAuthorize` interceptor.
|
||||
|
||||
@@ -291,8 +311,10 @@ When intercepting coroutines, only the first interceptor participates.
|
||||
If any other interceptors are present and come after Spring Security's method security interceptor, https://github.com/spring-projects/spring-framework/issues/22462[they will be skipped].
|
||||
====
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
|
||||
@@ -309,7 +331,8 @@ StepVerifier.create(messageByUsername)
|
||||
.verifyComplete();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER")
|
||||
@@ -324,12 +347,14 @@ StepVerifier.create(messageByUsername)
|
||||
.expectNext("Hi user")
|
||||
.verifyComplete()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
with `this::findMessageByUsername` defined as:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Mono<String> findMessageByUsername(String username) {
|
||||
@@ -337,19 +362,22 @@ Mono<String> findMessageByUsername(String username) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
fun findMessageByUsername(username: String): Mono<String> {
|
||||
return Mono.just("Hi $username")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Below is a minimal method security configuration when using method security in reactive applications.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
@@ -370,7 +398,8 @@ public class SecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
@@ -390,12 +419,14 @@ class SecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Consider the following class:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
@@ -407,7 +438,8 @@ public class HelloWorldMessageService {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
@@ -418,12 +450,14 @@ class HelloWorldMessageService {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Or, the following class using Kotlin coroutines:
|
||||
|
||||
====
|
||||
.Kotlin
|
||||
[tabs]
|
||||
======
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="primary"]
|
||||
----
|
||||
@Component
|
||||
@@ -435,7 +469,7 @@ class HelloWorldMessageService {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
Combined with our configuration above, `@PreAuthorize("hasRole('ADMIN')")` will ensure that `findByMessage` is only invoked by a user with the role `ADMIN`.
|
||||
@@ -445,8 +479,10 @@ This means that the expression must not block.
|
||||
|
||||
When integrating with xref:reactive/configuration/webflux.adoc#jc-webflux[WebFlux Security], the Reactor Context is automatically established by Spring Security according to the authenticated user.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -482,7 +518,8 @@ public class SecurityConfig {
|
||||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -513,6 +550,6 @@ class SecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You can find a complete sample in {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method]
|
||||
|
||||
@@ -14,8 +14,10 @@ You can find a few sample applications that demonstrate the code below:
|
||||
You can find a minimal WebFlux Security configuration below:
|
||||
|
||||
.Minimal WebFlux Security Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
-----
|
||||
|
||||
@@ -34,7 +36,8 @@ public class HelloWebfluxSecurityConfig {
|
||||
}
|
||||
-----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
-----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -51,7 +54,7 @@ class HelloWebfluxSecurityConfig {
|
||||
}
|
||||
}
|
||||
-----
|
||||
====
|
||||
======
|
||||
|
||||
This configuration provides form and http basic authentication, sets up authorization to require an authenticated user for accessing any page, sets up a default log in page and a default log out page, sets up security related HTTP headers, CSRF protection, and more.
|
||||
|
||||
@@ -60,8 +63,10 @@ This configuration provides form and http basic authentication, sets up authoriz
|
||||
You can find an explicit version of the minimal WebFlux Security configuration below:
|
||||
|
||||
.Explicit WebFlux Security Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
-----
|
||||
@Configuration
|
||||
@@ -91,9 +96,12 @@ public class HelloWebfluxSecurityConfig {
|
||||
}
|
||||
-----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
-----
|
||||
import org.springframework.security.config.web.server.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
class HelloWebfluxSecurityConfig {
|
||||
@@ -120,7 +128,10 @@ class HelloWebfluxSecurityConfig {
|
||||
}
|
||||
}
|
||||
-----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
Make sure that you import the `invoke` function in your Kotlin class, sometimes the IDE will not auto-import it causing compilation issues.
|
||||
|
||||
This configuration explicitly sets up all the same things as our minimal configuration.
|
||||
From here you can easily make the changes to the defaults.
|
||||
@@ -134,8 +145,10 @@ You can configure multiple `SecurityWebFilterChain` instances to separate config
|
||||
|
||||
For example, you can isolate configuration for URLs that start with `/api`, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -173,9 +186,12 @@ static class MultiSecurityHttpConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.web.server.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
open class MultiSecurityHttpConfig {
|
||||
@@ -211,7 +227,7 @@ open class MultiSecurityHttpConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
<1> Configure a `SecurityWebFilterChain` with an `@Order` to specify which `SecurityWebFilterChain` Spring Security should consider first
|
||||
<2> Use `PathPatternParserServerWebExchangeMatcher` to state that this `SecurityWebFilterChain` will only apply to URL paths that start with `/api/`
|
||||
|
||||
@@ -35,8 +35,10 @@ These defaults come from https://docs.angularjs.org/api/ng/service/$http#cross-s
|
||||
You can configure `CookieServerCsrfTokenRepository` in Java Configuration using:
|
||||
|
||||
.Store CSRF Token in a Cookie
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
-----
|
||||
@Bean
|
||||
@@ -48,7 +50,8 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
|
||||
}
|
||||
-----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
-----
|
||||
@Bean
|
||||
@@ -61,7 +64,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
-----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -78,8 +81,10 @@ However, it is simple to disable CSRF protection if it xref:features/exploits/cs
|
||||
The Java configuration below will disable CSRF protection.
|
||||
|
||||
.Disable CSRF Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -91,7 +96,8 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
-----
|
||||
@Bean
|
||||
@@ -104,7 +110,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
-----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-csrf-configure-request-handler]]
|
||||
==== Configure ServerCsrfTokenRequestHandler
|
||||
@@ -117,8 +123,10 @@ An alternate implementation `XorServerCsrfTokenRequestAttributeHandler` is avail
|
||||
You can configure `XorServerCsrfTokenRequestAttributeHandler` using the following Java configuration:
|
||||
|
||||
.Configure BREACH protection
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
-----
|
||||
@Bean
|
||||
@@ -132,7 +140,8 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
|
||||
}
|
||||
-----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
-----
|
||||
@Bean
|
||||
@@ -145,7 +154,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
-----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-csrf-include]]
|
||||
=== Include the CSRF Token
|
||||
@@ -161,8 +170,10 @@ If your view technology does not provide a simple way to subscribe to the `Mono<
|
||||
For example, the following code will place the `CsrfToken` on the default attribute name (`_csrf`) used by Spring Security's <<webflux-csrf-include-form-auto,CsrfRequestDataValueProcessor>> to automatically include the CSRF token as a hidden input.
|
||||
|
||||
.`CsrfToken` as `@ModelAttribute`
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@ControllerAdvice
|
||||
@@ -176,7 +187,8 @@ public class SecurityControllerAdvice {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@ControllerAdvice
|
||||
@@ -190,7 +202,7 @@ class SecurityControllerAdvice {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Fortunately, Thymeleaf provides <<webflux-csrf-include-form-auto,integration>> that works without any additional work.
|
||||
|
||||
@@ -200,14 +212,12 @@ In order to post an HTML form the CSRF token must be included in the form as a h
|
||||
For example, the rendered HTML might look like:
|
||||
|
||||
.CSRF Token HTML
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<input type="hidden"
|
||||
name="_csrf"
|
||||
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
|
||||
----
|
||||
====
|
||||
|
||||
Next we will discuss various ways of including the CSRF token in a form as a hidden input.
|
||||
|
||||
@@ -227,7 +237,6 @@ If the <<webflux-csrf-include,other options>> for including the actual CSRF toke
|
||||
The Thymeleaf sample below assumes that you <<webflux-csrf-include-subscribe,expose>> the `CsrfToken` on an attribute named `_csrf`.
|
||||
|
||||
.CSRF Token in Form with Request Attribute
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form th:action="@{/logout}"
|
||||
@@ -239,7 +248,6 @@ The Thymeleaf sample below assumes that you <<webflux-csrf-include-subscribe,exp
|
||||
th:value="${_csrf.token}"/>
|
||||
</form>
|
||||
----
|
||||
====
|
||||
|
||||
[[webflux-csrf-include-ajax]]
|
||||
==== Ajax and JSON Requests
|
||||
@@ -261,7 +269,6 @@ An alternative pattern to <<webflux-csrf-include-form-auto,exposing the CSRF in
|
||||
The HTML might look something like this:
|
||||
|
||||
.CSRF meta tag HTML
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<html>
|
||||
@@ -272,13 +279,11 @@ The HTML might look something like this:
|
||||
</head>
|
||||
<!-- ... -->
|
||||
----
|
||||
====
|
||||
|
||||
Once the meta tags contained the CSRF token, the JavaScript code would read the meta tags and include the CSRF token as a header.
|
||||
If you were using jQuery, this could be done with the following:
|
||||
|
||||
.AJAX send CSRF Token
|
||||
====
|
||||
[source,javascript]
|
||||
----
|
||||
$(function () {
|
||||
@@ -289,13 +294,11 @@ $(function () {
|
||||
});
|
||||
});
|
||||
----
|
||||
====
|
||||
|
||||
The sample below assumes that you <<webflux-csrf-include-subscribe,expose>> the `CsrfToken` on an attribute named `_csrf`.
|
||||
An example of doing this with Thymeleaf is shown below:
|
||||
|
||||
.CSRF meta tag JSP
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<html>
|
||||
@@ -307,7 +310,6 @@ An example of doing this with Thymeleaf is shown below:
|
||||
</head>
|
||||
<!-- ... -->
|
||||
----
|
||||
====
|
||||
|
||||
[[webflux-csrf-considerations]]
|
||||
== CSRF Considerations
|
||||
@@ -339,8 +341,10 @@ For example, the following Java Configuration will perform logout with the URL `
|
||||
// FIXME: This should be a link to log out documentation
|
||||
|
||||
.Log out with HTTP GET
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -352,7 +356,8 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -365,7 +370,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-considerations-csrf-timeouts]]
|
||||
@@ -401,8 +406,10 @@ We have xref:features/exploits/csrf.adoc#csrf-considerations-multipart[already d
|
||||
In a WebFlux application, this can be configured with the following configuration:
|
||||
|
||||
.Enable obtaining CSRF token from multipart/form-data
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -414,7 +421,8 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -427,7 +435,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-csrf-considerations-multipart-url]]
|
||||
==== Include CSRF Token in URL
|
||||
@@ -437,14 +445,12 @@ Since the `CsrfToken` is exposed as an `ServerHttpRequest` <<webflux-csrf-includ
|
||||
An example with Thymeleaf is shown below:
|
||||
|
||||
.CSRF Token in Action
|
||||
====
|
||||
[source,html]
|
||||
----
|
||||
<form method="post"
|
||||
th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
|
||||
enctype="multipart/form-data">
|
||||
----
|
||||
====
|
||||
|
||||
[[webflux-csrf-considerations-override-method]]
|
||||
=== HiddenHttpMethodFilter
|
||||
|
||||
@@ -16,8 +16,10 @@ For example, assume that you want the defaults except you wish to specify `SAMEO
|
||||
You can easily do this with the following Configuration:
|
||||
|
||||
.Customize Default Security Headers
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -33,7 +35,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -48,14 +51,16 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you do not want the defaults to be added and want explicit control over what should be used, you can disable the defaults.
|
||||
An example is provided below:
|
||||
|
||||
.Disable HTTP Security Response Headers
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -67,7 +72,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -80,7 +86,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-cache-control]]
|
||||
== Cache Control
|
||||
@@ -96,8 +102,10 @@ Details on how to do this can be found in the https://docs.spring.io/spring/docs
|
||||
If necessary, you can also disable Spring Security's cache control HTTP response headers.
|
||||
|
||||
.Cache Control Disabled
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -111,7 +119,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -126,7 +135,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-headers-content-type-options]]
|
||||
@@ -135,8 +144,10 @@ Spring Security includes xref:features/exploits/headers.adoc#headers-content-typ
|
||||
However, you can disable it with:
|
||||
|
||||
.Content Type Options Disabled
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -150,7 +161,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -165,7 +177,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-hsts]]
|
||||
== HTTP Strict Transport Security (HSTS)
|
||||
@@ -174,8 +186,10 @@ However, you can customize the results explicitly.
|
||||
For example, the following is an example of explicitly providing HSTS:
|
||||
|
||||
.Strict Transport Security
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -193,7 +207,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -210,7 +225,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-frame-options]]
|
||||
== X-Frame-Options
|
||||
@@ -219,8 +234,10 @@ By default, Spring Security disables rendering within an iframe using xref:featu
|
||||
You can customize frame options to use the same origin using the following:
|
||||
|
||||
.X-Frame-Options: SAMEORIGIN
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -236,7 +253,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -251,7 +269,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-xss-protection]]
|
||||
== X-XSS-Protection
|
||||
@@ -259,8 +277,10 @@ By default, Spring Security instructs browsers to block reflected XSS attacks us
|
||||
You can disable `X-XSS-Protection` with the following Configuration:
|
||||
|
||||
.X-XSS-Protection Customization
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -274,7 +294,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -289,7 +310,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-csp]]
|
||||
== Content Security Policy (CSP)
|
||||
@@ -299,18 +320,18 @@ The web application author must declare the security policy(s) to enforce and/or
|
||||
For example, given the following security policy:
|
||||
|
||||
.Content Security Policy Example
|
||||
====
|
||||
[source,http]
|
||||
----
|
||||
Content-Security-Policy: script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/
|
||||
----
|
||||
====
|
||||
|
||||
You can enable the CSP header as shown below:
|
||||
|
||||
.Content Security Policy
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -326,7 +347,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -341,13 +363,15 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
To enable the CSP `report-only` header, provide the following configuration:
|
||||
|
||||
.Content Security Policy Report Only
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -364,7 +388,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -380,17 +405,19 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-referrer]]
|
||||
== Referrer Policy
|
||||
|
||||
Spring Security does not add xref:features/exploits/headers.adoc#headers-referrer[Referrer Policy] headers by default.
|
||||
You can enable the Referrer Policy header using configuration as shown below:
|
||||
Spring Security adds the xref:features/exploits/headers.adoc#headers-referrer[Referrer Policy] header by default with the directive `no-referrer`.
|
||||
You can change the Referrer Policy header using configuration as shown below:
|
||||
|
||||
.Referrer Policy Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -406,7 +433,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -421,7 +449,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-headers-feature]]
|
||||
@@ -431,18 +459,18 @@ Spring Security does not add xref:features/exploits/headers.adoc#headers-feature
|
||||
The following `Feature-Policy` header:
|
||||
|
||||
.Feature-Policy Example
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Feature-Policy: geolocation 'self'
|
||||
----
|
||||
====
|
||||
|
||||
You can enable the Feature Policy header as shown below:
|
||||
|
||||
.Feature-Policy Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -456,7 +484,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -469,7 +498,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-headers-permissions]]
|
||||
@@ -479,18 +508,18 @@ Spring Security does not add xref:features/exploits/headers.adoc#headers-permiss
|
||||
The following `Permissions-Policy` header:
|
||||
|
||||
.Permissions-Policy Example
|
||||
====
|
||||
[source]
|
||||
----
|
||||
Permissions-Policy: geolocation=(self)
|
||||
----
|
||||
====
|
||||
|
||||
You can enable the Permissions Policy header as shown below:
|
||||
|
||||
.Permissions-Policy Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -506,7 +535,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -521,7 +551,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-headers-clear-site-data]]
|
||||
@@ -531,17 +561,17 @@ Spring Security does not add xref:features/exploits/headers.adoc#headers-clear-s
|
||||
The following Clear-Site-Data header:
|
||||
|
||||
.Clear-Site-Data Example
|
||||
====
|
||||
----
|
||||
Clear-Site-Data: "cache", "cookies"
|
||||
----
|
||||
====
|
||||
|
||||
can be sent on log out with the following configuration:
|
||||
|
||||
.Clear-Site-Data Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -559,7 +589,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -577,7 +608,7 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-headers-cross-origin-policies]]
|
||||
== Cross-Origin Policies
|
||||
@@ -595,8 +626,10 @@ Spring Security does not add <<headers-cross-origin-policies,Cross-Origin Polici
|
||||
The headers can be added with the following configuration:
|
||||
|
||||
.Cross-Origin Policies
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -613,7 +646,9 @@ public class WebSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
.Kotlin
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -631,7 +666,7 @@ open class CrossOriginPoliciesCustomConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This configuration will write the headers with the values provided:
|
||||
[source]
|
||||
|
||||
@@ -13,8 +13,10 @@ If a client makes a request using HTTP rather than HTTPS, Spring Security can be
|
||||
For example, the following Java configuration will redirect any HTTP requests to HTTPS:
|
||||
|
||||
.Redirect to HTTPS
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -26,7 +28,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -37,15 +40,17 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The configuration can easily be wrapped around an if statement to only be turned on in production.
|
||||
Alternatively, it can be enabled by looking for a property about the request that only happens in production.
|
||||
For example, if the production environment adds a header named `X-Forwarded-Proto` the following Java Configuration could be used:
|
||||
|
||||
.Redirect to HTTPS when X-Forwarded
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -59,7 +64,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -74,7 +80,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-hsts]]
|
||||
== Strict Transport Security
|
||||
|
||||
@@ -14,8 +14,10 @@ For your convenience, you can download a minimal Reactive Spring Boot + Spring S
|
||||
|
||||
You can add Spring Security to your Spring Boot project by adding `spring-boot-starter-security`.
|
||||
|
||||
====
|
||||
.Maven
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<dependency>
|
||||
@@ -24,12 +26,13 @@ You can add Spring Security to your Spring Boot project by adding `spring-boot-s
|
||||
</dependency>
|
||||
----
|
||||
|
||||
.Gradle
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary"]
|
||||
----
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[servlet-hello-starting]]
|
||||
@@ -38,10 +41,12 @@ You can add Spring Security to your Spring Boot project by adding `spring-boot-s
|
||||
You can now https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-running-with-the-maven-plugin[run the Spring Boot application] by using the Maven Plugin's `run` goal.
|
||||
The following example shows how to do so (and the beginning of the output from doing so):
|
||||
|
||||
.Running Spring Boot Application
|
||||
|
||||
====
|
||||
.Maven
|
||||
.Running Spring Boot Application
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,bash,role="primary"]
|
||||
----
|
||||
$ ./mvnw spring-boot:run
|
||||
@@ -53,7 +58,8 @@ Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
|
||||
...
|
||||
----
|
||||
|
||||
.Gradle
|
||||
Gradle::
|
||||
+
|
||||
[source,bash,role="secondary"]
|
||||
----
|
||||
$ ./gradlew bootRun
|
||||
@@ -64,7 +70,7 @@ Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
|
||||
|
||||
...
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[authenticating]]
|
||||
== Authenticating
|
||||
|
||||
@@ -10,8 +10,10 @@ The easiest way to ensure that CORS is handled first is to use the `CorsWebFilte
|
||||
Users can integrate the `CorsWebFilter` with Spring Security by providing a `CorsConfigurationSource`.
|
||||
For example, the following will integrate CORS support within Spring Security:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -25,7 +27,8 @@ CorsConfigurationSource corsConfigurationSource() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -38,12 +41,14 @@ fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
return source
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The following will disable the CORS integration within Spring Security:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -55,7 +60,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -68,4 +74,4 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -14,8 +14,10 @@ You can find a few sample applications that demonstrate the code below:
|
||||
|
||||
You can find a minimal RSocket Security configuration below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
-----
|
||||
@Configuration
|
||||
@@ -34,7 +36,8 @@ public class HelloRSocketSecurityConfig {
|
||||
}
|
||||
-----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -51,7 +54,7 @@ open class HelloRSocketSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This configuration enables <<rsocket-authentication-simple,simple authentication>> and sets up <<rsocket-authorization,rsocket-authorization>> to require an authenticated user for any request.
|
||||
|
||||
@@ -61,8 +64,10 @@ For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor`
|
||||
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
|
||||
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -71,7 +76,8 @@ RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInte
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -83,7 +89,7 @@ fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[rsocket-authentication]]
|
||||
== RSocket Authentication
|
||||
@@ -123,8 +129,10 @@ See `RSocketSecurity.basicAuthentication(Customizer)` for setting it up.
|
||||
The RSocket receiver can decode the credentials using `AuthenticationPayloadExchangeConverter` which is automatically setup using the `simpleAuthentication` portion of the DSL.
|
||||
An explicit configuration can be found below.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -140,7 +148,8 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -154,30 +163,35 @@ open fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInte
|
||||
return rsocket.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The RSocket sender can send credentials using `SimpleAuthenticationEncoder` which can be added to Spring's `RSocketStrategies`.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
RSocketStrategies.Builder strategies = ...;
|
||||
strategies.encoder(new SimpleAuthenticationEncoder());
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
var strategies: RSocketStrategies.Builder = ...
|
||||
strategies.encoder(SimpleAuthenticationEncoder())
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
It can then be used to send a username and password to the receiver in the setup:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
MimeType authenticationMimeType =
|
||||
@@ -189,7 +203,8 @@ Mono<RSocketRequester> requester = RSocketRequester.builder()
|
||||
.connectTcp(host, port);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val authenticationMimeType: MimeType =
|
||||
@@ -200,12 +215,14 @@ val requester: Mono<RSocketRequester> = RSocketRequester.builder()
|
||||
.rsocketStrategies(strategies.build())
|
||||
.connectTcp(host, port)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Alternatively or additionally, a username and password can be sent in a request.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Mono<RSocketRequester> requester;
|
||||
@@ -220,7 +237,8 @@ public Mono<AirportLocation> findRadar(String code) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.messaging.rsocket.retrieveMono
|
||||
@@ -238,7 +256,7 @@ open fun findRadar(code: String): Mono<AirportLocation> {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[rsocket-authentication-jwt]]
|
||||
=== JWT
|
||||
@@ -249,8 +267,10 @@ The support comes in the form of authenticating a JWT (determining the JWT is va
|
||||
The RSocket receiver can decode the credentials using `BearerPayloadExchangeConverter` which is automatically setup using the `jwt` portion of the DSL.
|
||||
An example configuration can be found below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -266,7 +286,8 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -280,13 +301,15 @@ fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorIntercept
|
||||
return rsocket.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The configuration above relies on the existence of a `ReactiveJwtDecoder` `@Bean` being present.
|
||||
An example of creating one from the issuer can be found below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -296,7 +319,8 @@ ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -305,13 +329,15 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
.fromIssuerLocation("https://example.com/auth/realms/demo")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The RSocket sender does not need to do anything special to send the token because the value is just a simple String.
|
||||
For example, the token can be sent at setup time:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
MimeType authenticationMimeType =
|
||||
@@ -322,7 +348,8 @@ Mono<RSocketRequester> requester = RSocketRequester.builder()
|
||||
.connectTcp(host, port);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val authenticationMimeType: MimeType =
|
||||
@@ -333,12 +360,14 @@ val requester = RSocketRequester.builder()
|
||||
.setupMetadata(token, authenticationMimeType)
|
||||
.connectTcp(host, port)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Alternatively or additionally, the token can be sent in a request.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
MimeType authenticationMimeType =
|
||||
@@ -355,7 +384,8 @@ public Mono<AirportLocation> findRadar(String code) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val authenticationMimeType: MimeType =
|
||||
@@ -371,7 +401,7 @@ open fun findRadar(code: String): Mono<AirportLocation> {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[rsocket-authorization]]
|
||||
== RSocket Authorization
|
||||
@@ -380,8 +410,10 @@ RSocket authorization is performed with `AuthorizationPayloadInterceptor` which
|
||||
The DSL can be used to setup authorization rules based upon the `PayloadExchange`.
|
||||
An example configuration can be found below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
rsocket
|
||||
@@ -397,7 +429,9 @@ rsocket
|
||||
.anyExchange().permitAll() // <6>
|
||||
);
|
||||
----
|
||||
.Kotlin
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
rsocket
|
||||
@@ -413,7 +447,7 @@ rsocket
|
||||
.anyExchange().permitAll()
|
||||
} // <6>
|
||||
----
|
||||
====
|
||||
======
|
||||
<1> Setting up a connection requires the authority `ROLE_SETUP`
|
||||
<2> If the route is `fetch.profile.me` authorization only requires the user be authenticated
|
||||
<3> In this rule we setup a custom matcher where authorization requires the user to have the authority `ROLE_CUSTOM`
|
||||
|
||||
@@ -111,8 +111,10 @@ OPTIONAL. Space delimited, case sensitive list of ASCII string values that speci
|
||||
|
||||
The following example shows how to configure the `DefaultServerOAuth2AuthorizationRequestResolver` with a `Consumer<OAuth2AuthorizationRequest.Builder>` that customizes the Authorization Request for `oauth2Login()`, by including the request parameter `prompt=consent`.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -154,7 +156,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -192,7 +195,7 @@ class SecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
For the simple use case, where the additional request parameter is always the same for a specific provider, it may be added directly in the `authorization-uri` property.
|
||||
|
||||
@@ -217,8 +220,10 @@ Alternatively, if your requirements are more advanced, you can take full control
|
||||
|
||||
The following example shows a variation of `authorizationRequestCustomizer()` from the preceding example, and instead overrides the `OAuth2AuthorizationRequest.authorizationRequestUri` property.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
|
||||
@@ -228,7 +233,8 @@ private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomi
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
private fun authorizationRequestCustomizer(): Consumer<OAuth2AuthorizationRequest.Builder> {
|
||||
@@ -241,7 +247,7 @@ private fun authorizationRequestCustomizer(): Consumer<OAuth2AuthorizationReques
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
=== Storing the Authorization Request
|
||||
@@ -256,8 +262,10 @@ The default implementation of `ServerAuthorizationRequestRepository` is `WebSess
|
||||
If you have a custom implementation of `ServerAuthorizationRequestRepository`, you may configure it as shown in the following example:
|
||||
|
||||
.ServerAuthorizationRequestRepository Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -275,7 +283,8 @@ public class OAuth2ClientSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -291,7 +300,7 @@ class OAuth2ClientSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Requesting an Access Token
|
||||
|
||||
@@ -327,8 +336,10 @@ Alternatively, if your requirements are more advanced, you can take full control
|
||||
Whether you customize `WebClientReactiveAuthorizationCodeTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you’ll need to configure it as shown in the following example:
|
||||
|
||||
.Access Token Response Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -354,7 +365,8 @@ public class OAuth2ClientSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -377,7 +389,7 @@ class OAuth2ClientSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[oauth2Client-refresh-token-grant]]
|
||||
@@ -421,8 +433,10 @@ Alternatively, if your requirements are more advanced, you can take full control
|
||||
Whether you customize `WebClientReactiveRefreshTokenTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you’ll need to configure it as shown in the following example:
|
||||
|
||||
.Access Token Response Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -439,7 +453,8 @@ ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -454,7 +469,7 @@ val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveO
|
||||
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`ReactiveOAuth2AuthorizedClientProviderBuilder.builder().refreshToken()` configures a `RefreshTokenReactiveOAuth2AuthorizedClientProvider`,
|
||||
@@ -504,8 +519,10 @@ Alternatively, if your requirements are more advanced, you can take full control
|
||||
|
||||
Whether you customize `WebClientReactiveClientCredentialsTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -521,7 +538,8 @@ ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -535,7 +553,7 @@ val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveO
|
||||
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()` configures a `ClientCredentialsReactiveOAuth2AuthorizedClientProvider`,
|
||||
@@ -564,8 +582,10 @@ spring:
|
||||
|
||||
...and the `ReactiveOAuth2AuthorizedClientManager` `@Bean`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -587,7 +607,8 @@ public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -603,12 +624,14 @@ fun authorizedClientManager(
|
||||
return authorizedClientManager
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You may obtain the `OAuth2AccessToken` as follows:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -632,7 +655,8 @@ public class OAuth2ClientController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class OAuth2ClientController {
|
||||
@@ -654,7 +678,7 @@ class OAuth2ClientController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`ServerWebExchange` is an OPTIONAL attribute.
|
||||
@@ -701,8 +725,10 @@ Alternatively, if your requirements are more advanced, you can take full control
|
||||
|
||||
Whether you customize `WebClientReactivePasswordTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -719,7 +745,8 @@ ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val passwordTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> = ...
|
||||
@@ -733,7 +760,7 @@ val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.bui
|
||||
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`ReactiveOAuth2AuthorizedClientProviderBuilder.builder().password()` configures a `PasswordReactiveOAuth2AuthorizedClientProvider`,
|
||||
@@ -762,8 +789,10 @@ spring:
|
||||
|
||||
...and the `ReactiveOAuth2AuthorizedClientManager` `@Bean`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -807,7 +836,9 @@ private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttri
|
||||
};
|
||||
}
|
||||
----
|
||||
.Kotlin
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -846,12 +877,14 @@ private fun contextAttributesMapper(): Function<OAuth2AuthorizeRequest, Mono<Mut
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You may obtain the `OAuth2AccessToken` as follows:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -875,7 +908,8 @@ public class OAuth2ClientController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -897,7 +931,7 @@ class OAuth2ClientController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`ServerWebExchange` is an OPTIONAL attribute.
|
||||
@@ -943,8 +977,10 @@ Alternatively, if your requirements are more advanced, you can take full control
|
||||
|
||||
Whether you customize `WebClientReactiveJwtBearerTokenResponseClient` or provide your own implementation of `ReactiveOAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -963,7 +999,8 @@ ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
// Customize
|
||||
@@ -980,7 +1017,7 @@ val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.bui
|
||||
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Using the Access Token
|
||||
|
||||
@@ -1005,8 +1042,10 @@ spring:
|
||||
|
||||
...and the `OAuth2AuthorizedClientManager` `@Bean`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -1031,7 +1070,8 @@ public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -1048,12 +1088,14 @@ fun authorizedClientManager(
|
||||
return authorizedClientManager
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You may obtain the `OAuth2AccessToken` as follows:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
@@ -1075,7 +1117,8 @@ public class OAuth2ResourceServerController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class OAuth2ResourceServerController {
|
||||
@@ -1094,7 +1137,7 @@ class OAuth2ResourceServerController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`JwtBearerReactiveOAuth2AuthorizedClientProvider` resolves the `Jwt` assertion via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example.
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`.
|
||||
This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `ReactiveOAuth2AuthorizedClientManager` or `ReactiveOAuth2AuthorizedClientService`.
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -24,7 +26,8 @@ public class OAuth2ClientController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -37,7 +40,7 @@ class OAuth2ClientController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses a <<oauth2Client-authorized-manager-provider, ReactiveOAuth2AuthorizedClientManager>> and therefore inherits it's capabilities.
|
||||
|
||||
@@ -58,8 +61,10 @@ It directly uses an <<oauth2Client-authorized-manager-provider, ReactiveOAuth2Au
|
||||
|
||||
The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -72,7 +77,8 @@ WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManage
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -83,7 +89,7 @@ fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): W
|
||||
.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Providing the Authorized Client
|
||||
|
||||
@@ -91,8 +97,10 @@ The `ServerOAuth2AuthorizedClientExchangeFilterFunction` determines the client t
|
||||
|
||||
The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -110,7 +118,8 @@ public Mono<String> index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2Author
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -127,14 +136,16 @@ fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2Auth
|
||||
.thenReturn("index")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
<1> `oauth2AuthorizedClient()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`.
|
||||
|
||||
The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -152,7 +163,8 @@ public Mono<String> index() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -169,7 +181,7 @@ fun index(): Mono<String> {
|
||||
.thenReturn("index")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
<1> `clientRegistrationId()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`.
|
||||
|
||||
|
||||
@@ -181,8 +193,10 @@ If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authe
|
||||
|
||||
The following code shows the specific configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -196,7 +210,8 @@ WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManage
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -208,7 +223,7 @@ fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): W
|
||||
.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[WARNING]
|
||||
It is recommended to be cautious with this feature since all HTTP requests will receive the access token.
|
||||
@@ -217,8 +232,10 @@ Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a
|
||||
|
||||
The following code shows the specific configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -232,7 +249,8 @@ WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManage
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -244,7 +262,7 @@ fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): W
|
||||
.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[WARNING]
|
||||
It is recommended to be cautious with this feature since all HTTP requests will receive the access token.
|
||||
|
||||
@@ -36,8 +36,10 @@ spring:
|
||||
|
||||
The following example shows how to configure `WebClientReactiveAuthorizationCodeTokenResponseClient`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
|
||||
@@ -59,7 +61,8 @@ tokenResponseClient.addParametersConverter(
|
||||
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val jwkResolver: Function<ClientRegistration, JWK> =
|
||||
@@ -81,7 +84,7 @@ tokenResponseClient.addParametersConverter(
|
||||
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
|
||||
)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
=== Authenticate using `client_secret_jwt`
|
||||
@@ -105,8 +108,10 @@ spring:
|
||||
|
||||
The following example shows how to configure `WebClientReactiveClientCredentialsTokenResponseClient`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
|
||||
@@ -127,7 +132,8 @@ tokenResponseClient.addParametersConverter(
|
||||
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val jwkResolver = Function<ClientRegistration, JWK?> { clientRegistration: ClientRegistration ->
|
||||
@@ -148,14 +154,16 @@ tokenResponseClient.addParametersConverter(
|
||||
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
|
||||
)
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Customizing the JWT assertion
|
||||
|
||||
The JWT produced by `NimbusJwtClientAuthenticationParametersConverter` contains the `iss`, `sub`, `aud`, `jti`, `iat` and `exp` claims by default. You can customize the headers and/or claims by providing a `Consumer<NimbusJwtClientAuthenticationParametersConverter.JwtClientAuthenticationContext<T>>` to `setJwtClientAssertionCustomizer()`. The following example shows how to customize claims of the JWT:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Function<ClientRegistration, JWK> jwkResolver = ...
|
||||
@@ -168,7 +176,8 @@ converter.setJwtClientAssertionCustomizer((context) -> {
|
||||
});
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val jwkResolver = ...
|
||||
@@ -180,4 +189,4 @@ converter.setJwtClientAssertionCustomizer { context ->
|
||||
context.claims.claim("custom-claim", "claim-value")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -69,20 +69,23 @@ A `ClientRegistration` can be initially configured using discovery of an OpenID
|
||||
|
||||
`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
ClientRegistration clientRegistration =
|
||||
ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response.
|
||||
|
||||
@@ -106,8 +109,10 @@ The auto-configuration also registers the `ReactiveClientRegistrationRepository`
|
||||
|
||||
The following listing shows an example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -125,7 +130,8 @@ public class OAuth2ClientController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -142,7 +148,7 @@ class OAuth2ClientController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[oauth2Client-authorized-client]]
|
||||
== OAuth2AuthorizedClient
|
||||
@@ -163,8 +169,10 @@ From a developer perspective, the `ServerOAuth2AuthorizedClientRepository` or `R
|
||||
|
||||
The following listing shows an example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -183,7 +191,8 @@ public class OAuth2ClientController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -201,7 +210,7 @@ class OAuth2ClientController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
Spring Boot 2.x auto-configuration registers an `ServerOAuth2AuthorizedClientRepository` and/or `ReactiveOAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`.
|
||||
@@ -235,8 +244,10 @@ The `ReactiveOAuth2AuthorizedClientProviderBuilder` may be used to configure and
|
||||
|
||||
The following code shows an example of how to configure and build a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -261,7 +272,8 @@ public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -280,7 +292,7 @@ fun authorizedClientManager(
|
||||
return authorizedClientManager
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
When an authorization attempt succeeds, the `DefaultReactiveOAuth2AuthorizedClientManager` will delegate to the `ReactiveOAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `ServerOAuth2AuthorizedClientRepository`.
|
||||
In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `ServerOAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler`.
|
||||
@@ -291,8 +303,10 @@ This can be useful when you need to supply a `ReactiveOAuth2AuthorizedClientProv
|
||||
|
||||
The following code shows an example of the `contextAttributesMapper`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -337,7 +351,8 @@ private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttri
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -376,7 +391,7 @@ private fun contextAttributesMapper(): Function<OAuth2AuthorizeRequest, Mono<Mut
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The `DefaultReactiveOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `ServerWebExchange`.
|
||||
When operating *_outside_* of a `ServerWebExchange` context, use `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` instead.
|
||||
@@ -387,8 +402,10 @@ An OAuth 2.0 Client configured with the `client_credentials` grant type can be c
|
||||
|
||||
The following code shows an example of how to configure an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -410,7 +427,8 @@ public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -426,4 +444,4 @@ fun authorizedClientManager(
|
||||
return authorizedClientManager
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -24,8 +24,10 @@ The `ServerHttpSecurity.oauth2Client()` DSL provides a number of configuration o
|
||||
The following code shows the complete configuration options provided by the `ServerHttpSecurity.oauth2Client()` DSL:
|
||||
|
||||
.OAuth2 Client Configuration Options
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -47,7 +49,8 @@ public class OAuth2ClientSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -67,14 +70,16 @@ class OAuth2ClientSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The `ReactiveOAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `ReactiveOAuth2AuthorizedClientProvider`(s).
|
||||
|
||||
The following code shows an example of how to register a `ReactiveOAuth2AuthorizedClientManager` `@Bean` and associate it with a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -99,7 +104,8 @@ public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -118,4 +124,4 @@ fun authorizedClientManager(
|
||||
return authorizedClientManager
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -23,8 +23,10 @@ These claims are normally represented by a JSON object that contains a collectio
|
||||
The following code shows the complete configuration options available for the `oauth2Login()` DSL:
|
||||
|
||||
.OAuth2 Login Configuration Options
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -52,7 +54,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -78,7 +81,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The following sections go into more detail on each of the configuration options available:
|
||||
|
||||
@@ -115,8 +118,10 @@ To override the default login page, configure the `exceptionHandling().authentic
|
||||
The following listing shows an example:
|
||||
|
||||
.OAuth2 Login Page Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -148,7 +153,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -179,7 +185,7 @@ class OAuth2LoginSecurityConfig {
|
||||
...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[IMPORTANT]
|
||||
You need to provide a `@Controller` with a `@RequestMapping("/login/oauth2")` that is capable of rendering the custom login page.
|
||||
@@ -207,13 +213,15 @@ The Redirection Endpoint is used by the Authorization Server for returning the A
|
||||
OAuth 2.0 Login leverages the Authorization Code Grant.
|
||||
Therefore, the authorization credential is the authorization code.
|
||||
|
||||
The default Authorization Response redirection endpoint is `/login/oauth2/code/{registrationId}`.
|
||||
The default Authorization Response redirection endpoint is `+/login/oauth2/code/{registrationId}+`.
|
||||
|
||||
If you would like to customize the Authorization Response redirection endpoint, configure it as shown in the following example:
|
||||
|
||||
.Redirection Endpoint Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -231,7 +239,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -247,7 +256,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
@@ -255,7 +264,10 @@ You also need to ensure the `ClientRegistration.redirectUri` matches the custom
|
||||
|
||||
The following listing shows an example:
|
||||
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
@@ -265,7 +277,8 @@ return CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
@@ -274,6 +287,7 @@ return CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
|
||||
.build()
|
||||
----
|
||||
======
|
||||
====
|
||||
|
||||
|
||||
@@ -307,8 +321,10 @@ There are a couple of options to choose from when mapping user authorities:
|
||||
Register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the configuration, as shown in the following example:
|
||||
|
||||
.Granted Authorities Mapper Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -355,7 +371,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -389,7 +406,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2-login-advanced-map-authorities-reactiveoauth2userservice]]
|
||||
==== Delegation-based strategy with ReactiveOAuth2UserService
|
||||
@@ -401,8 +418,10 @@ The `OAuth2UserRequest` (and `OidcUserRequest`) provides you access to the assoc
|
||||
The following example shows how to implement and configure a delegation-based strategy using an OpenID Connect 1.0 UserService:
|
||||
|
||||
.ReactiveOAuth2UserService Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -442,7 +461,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -478,7 +498,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-oauth2-login-advanced-oauth2-user-service]]
|
||||
@@ -495,8 +515,10 @@ If you need to customize the pre-processing of the UserInfo Request and/or the p
|
||||
|
||||
Whether you customize `DefaultReactiveOAuth2UserService` or provide your own implementation of `ReactiveOAuth2UserService`, you'll need to configure it as shown in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -518,7 +540,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -537,7 +560,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-oauth2-login-advanced-oidc-user-service]]
|
||||
@@ -551,8 +574,10 @@ If you need to customize the pre-processing of the UserInfo Request and/or the p
|
||||
|
||||
Whether you customize `OidcReactiveOAuth2UserService` or provide your own implementation of `ReactiveOAuth2UserService` for OpenID Connect 1.0 Provider's, you'll need to configure it as shown in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -574,7 +599,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -593,7 +619,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-oauth2-login-advanced-idtoken-verify]]
|
||||
@@ -610,8 +636,10 @@ The JWS algorithm resolver is a `Function` that accepts a `ClientRegistration` a
|
||||
|
||||
The following code shows how to configure the `OidcIdTokenDecoderFactory` `@Bean` to default to `MacAlgorithm.HS256` for all `ClientRegistration`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -622,7 +650,8 @@ public ReactiveJwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -632,7 +661,7 @@ fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory<ClientRegistration> {
|
||||
return idTokenDecoderFactory
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
For MAC based algorithms such as `HS256`, `HS384` or `HS512`, the `client-secret` corresponding to the `client-id` is used as the symmetric key for signature verification.
|
||||
@@ -668,8 +697,10 @@ spring:
|
||||
|
||||
...and the `OidcClientInitiatedServerLogoutSuccessHandler`, which implements RP-Initiated Logout, may be configured as follows:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -705,7 +736,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -737,7 +769,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
NOTE: `OidcClientInitiatedServerLogoutSuccessHandler` supports the `{baseUrl}` placeholder.
|
||||
NOTE: `OidcClientInitiatedServerLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder.
|
||||
If used, the application's base URL, like `https://app.example.org`, will replace it at request time.
|
||||
|
||||
@@ -61,10 +61,8 @@ spring:
|
||||
----
|
||||
+
|
||||
.OAuth Client properties
|
||||
====
|
||||
<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
|
||||
<2> Following the base property prefix is the ID for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google.
|
||||
====
|
||||
|
||||
. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
|
||||
|
||||
@@ -244,8 +242,10 @@ If you need to override the auto-configuration based on your specific requiremen
|
||||
|
||||
The following example shows how to register a `ReactiveClientRegistrationRepository` `@Bean`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -275,7 +275,8 @@ public class OAuth2LoginConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@Configuration
|
||||
@@ -304,7 +305,7 @@ class OAuth2LoginConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-oauth2-login-register-securitywebfilterchain-bean]]
|
||||
@@ -313,8 +314,10 @@ class OAuth2LoginConfig {
|
||||
The following example shows how to register a `SecurityWebFilterChain` `@Bean` with `@EnableWebFluxSecurity` and enable OAuth 2.0 login through `serverHttpSecurity.oauth2Login()`:
|
||||
|
||||
.OAuth2 Login Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -333,7 +336,8 @@ public class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -350,7 +354,7 @@ class OAuth2LoginSecurityConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-oauth2-login-completely-override-autoconfiguration]]
|
||||
@@ -359,8 +363,10 @@ class OAuth2LoginSecurityConfig {
|
||||
The following example shows how to completely override the auto-configuration by registering a `ReactiveClientRegistrationRepository` `@Bean` and a `SecurityWebFilterChain` `@Bean`.
|
||||
|
||||
.Overriding the auto-configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -401,7 +407,8 @@ public class OAuth2LoginConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -440,7 +447,7 @@ class OAuth2LoginConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[webflux-oauth2-login-javaconfig-wo-boot]]
|
||||
@@ -449,8 +456,10 @@ class OAuth2LoginConfig {
|
||||
If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration:
|
||||
|
||||
.OAuth2 Login Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -493,7 +502,8 @@ public class OAuth2LoginConfig {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -536,4 +546,4 @@ class OAuth2LoginConfig {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -10,8 +10,10 @@ For example, you may have a need to read the bearer token from a custom header.
|
||||
To achieve this, you can wire an instance of `ServerBearerTokenAuthenticationConverter` into the DSL, as you can see in the following example:
|
||||
|
||||
.Custom Bearer Token Header
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter();
|
||||
@@ -22,7 +24,8 @@ http
|
||||
);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val converter = ServerBearerTokenAuthenticationConverter()
|
||||
@@ -33,15 +36,17 @@ return http {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Bearer Token Propagation
|
||||
|
||||
Now that you're in possession of a bearer token, it might be handy to pass that to downstream services.
|
||||
This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.html[ServerBearerExchangeFilterFunction]`, which you can see in the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -52,7 +57,8 @@ public WebClient rest() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -62,15 +68,17 @@ fun rest(): WebClient {
|
||||
.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential.
|
||||
Then, it will propagate that token in the `Authorization` header.
|
||||
|
||||
For example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
this.rest.get()
|
||||
@@ -79,7 +87,8 @@ this.rest.get()
|
||||
.bodyToMono(String.class)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
this.rest.get()
|
||||
@@ -87,14 +96,16 @@ this.rest.get()
|
||||
.retrieve()
|
||||
.bodyToMono<String>()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Will invoke the `https://other-service.example.com/endpoint`, adding the bearer token `Authorization` header for you.
|
||||
|
||||
In places where you need to override this behavior, it's a simple matter of supplying the header yourself, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
this.rest.get()
|
||||
@@ -104,7 +115,8 @@ this.rest.get()
|
||||
.bodyToMono(String.class)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
rest.get()
|
||||
@@ -113,7 +125,7 @@ rest.get()
|
||||
.retrieve()
|
||||
.bodyToMono<String>()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain.
|
||||
|
||||
|
||||
@@ -112,8 +112,10 @@ There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf.
|
||||
The first is a `SecurityWebFilterChain` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `SecurityWebFilterChain` looks like:
|
||||
|
||||
.Resource Server SecurityWebFilterChain
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -127,7 +129,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -142,15 +145,17 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
Replacing this is as simple as exposing the bean within the application:
|
||||
|
||||
.Replacing SecurityWebFilterChain
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -167,7 +172,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -183,7 +189,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
@@ -192,8 +198,10 @@ Methods on the `oauth2ResourceServer` DSL will also override or replace auto con
|
||||
For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
|
||||
|
||||
.ReactiveJwtDecoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -202,7 +210,8 @@ public ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -210,7 +219,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
Calling `{security-api-url}org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation]` is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri.
|
||||
@@ -223,8 +232,10 @@ And its configuration can be overridden using `jwkSetUri()` or replaced using `d
|
||||
|
||||
An authorization server's JWK Set Uri can be configured <<webflux-oauth2resourceserver-jwt-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -242,7 +253,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -259,7 +271,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Using `jwkSetUri()` takes precedence over any configuration property.
|
||||
|
||||
@@ -268,8 +280,10 @@ Using `jwkSetUri()` takes precedence over any configuration property.
|
||||
|
||||
More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -287,7 +301,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -304,7 +319,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This is handy when deeper configuration, like <<webflux-oauth2resourceserver-jwt-validation,validation>>, is necessary.
|
||||
|
||||
@@ -313,8 +328,10 @@ This is handy when deeper configuration, like <<webflux-oauth2resourceserver-jwt
|
||||
|
||||
Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -323,7 +340,8 @@ public ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -331,7 +349,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-algorithm]]
|
||||
== Configuring Trusted Algorithms
|
||||
@@ -361,8 +379,10 @@ spring:
|
||||
|
||||
For greater power, though, we can use a builder that ships with `NimbusReactiveJwtDecoder`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -372,7 +392,8 @@ ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -381,12 +402,14 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
.jwsAlgorithm(RS512).build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Calling `jwsAlgorithm` more than once will configure `NimbusReactiveJwtDecoder` to trust more than one algorithm, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -396,7 +419,8 @@ ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -405,12 +429,14 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Or, you can call `jwsAlgorithms`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -423,7 +449,8 @@ ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -436,7 +463,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-public-key]]
|
||||
=== Trusting a Single Asymmetric Key
|
||||
@@ -463,8 +490,10 @@ spring:
|
||||
Or, to allow for a more sophisticated lookup, you can post-process the `RsaKeyConversionServicePostProcessor`:
|
||||
|
||||
.BeanFactoryPostProcessor
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -475,7 +504,8 @@ BeanFactoryPostProcessor conversionServiceCustomizer() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -486,7 +516,7 @@ fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Specify your key's location:
|
||||
|
||||
@@ -497,29 +527,34 @@ key.location: hfds://my-key.pub
|
||||
|
||||
And then autowire the value:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Value("${key.location}")
|
||||
RSAPublicKey key;
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Value("\${key.location}")
|
||||
val key: RSAPublicKey? = null
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-public-key-builder]]
|
||||
==== Using a Builder
|
||||
|
||||
To wire an `RSAPublicKey` directly, you can simply use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -528,7 +563,8 @@ public ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -536,7 +572,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withPublicKey(key).build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-secret-key]]
|
||||
=== Trusting a Single Symmetric Key
|
||||
@@ -544,8 +580,10 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
Using a single symmetric key is also simple.
|
||||
You can simply load in your `SecretKey` and use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -554,7 +592,8 @@ public ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -562,7 +601,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-authorization]]
|
||||
=== Configuring Authorization
|
||||
@@ -575,8 +614,10 @@ When this is the case, Resource Server will attempt to coerce these scopes into
|
||||
|
||||
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -592,7 +633,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -609,25 +651,28 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Or similarly with method security:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public Flux<Message> getMessages(...) {}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
fun getMessages(): Flux<Message> { }
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-authorization-extraction]]
|
||||
==== Extracting Authorities Manually
|
||||
@@ -638,8 +683,10 @@ Or, at other times, the resource server may need to adapt the attribute or a com
|
||||
|
||||
To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -665,7 +712,8 @@ Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor()
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -688,15 +736,17 @@ fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationTok
|
||||
return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
which is responsible for converting a `Jwt` into an `Authentication`.
|
||||
As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities.
|
||||
|
||||
That final converter might be something like `GrantedAuthoritiesExtractor` below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
static class GrantedAuthoritiesExtractor
|
||||
@@ -714,7 +764,8 @@ static class GrantedAuthoritiesExtractor
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
|
||||
@@ -727,12 +778,14 @@ internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAu
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, Mono<AbstractAuthenticationToken>>`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
||||
@@ -742,7 +795,8 @@ static class CustomAuthenticationConverter implements Converter<Jwt, Mono<Abstra
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
||||
@@ -751,7 +805,7 @@ internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthe
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-validation]]
|
||||
=== Configuring Validation
|
||||
@@ -770,8 +824,10 @@ This can cause some implementation heartburn as the number of collaborating serv
|
||||
|
||||
Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -789,7 +845,8 @@ ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -802,7 +859,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 60 seconds.
|
||||
@@ -812,8 +869,10 @@ By default, Resource Server configures a clock skew of 60 seconds.
|
||||
|
||||
Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
@@ -829,7 +888,8 @@ public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class AudienceValidator : OAuth2TokenValidator<Jwt> {
|
||||
@@ -843,12 +903,14 @@ class AudienceValidator : OAuth2TokenValidator<Jwt> {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Then, to add into a resource server, it's a matter of specifying the `ReactiveJwtDecoder` instance:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -866,7 +928,8 @@ ReactiveJwtDecoder jwtDecoder() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -879,4 +942,4 @@ fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -17,8 +17,10 @@ In each case, there are two things that need to be done and trade-offs associate
|
||||
|
||||
One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerReactiveAuthenticationManagerResolver`, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver
|
||||
@@ -33,7 +35,8 @@ http
|
||||
);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
|
||||
@@ -47,7 +50,7 @@ return http {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This is nice because the issuer endpoints are loaded lazily.
|
||||
In fact, the corresponding `JwtReactiveAuthenticationManager` is instantiated only when the first request with the corresponding issuer is sent.
|
||||
@@ -58,8 +61,10 @@ This allows for an application startup that is independent from those authorizat
|
||||
Of course, you may not want to restart the application each time a new tenant is added.
|
||||
In this case, you can configure the `JwtIssuerReactiveAuthenticationManagerResolver` with a repository of `ReactiveAuthenticationManager` instances, which you can edit at runtime, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
private Mono<ReactiveAuthenticationManager> addManager(
|
||||
@@ -85,7 +90,8 @@ http
|
||||
);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
private fun addManager(
|
||||
@@ -108,7 +114,7 @@ return http {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In this case, you construct `JwtIssuerReactiveAuthenticationManagerResolver` with a strategy for obtaining the `ReactiveAuthenticationManager` given the issuer.
|
||||
This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime.
|
||||
|
||||
@@ -82,8 +82,10 @@ Once a token is authenticated, an instance of `BearerTokenAuthentication` is set
|
||||
|
||||
This means that it's available in `@Controller` methods when using `@EnableWebFlux` in your configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
@@ -92,7 +94,8 @@ public Mono<String> foo(BearerTokenAuthentication authentication) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
@@ -100,12 +103,14 @@ fun foo(authentication: BearerTokenAuthentication): Mono<String> {
|
||||
return Mono.just(authentication.tokenAttributes["sub"].toString() + " is the subject")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
@@ -114,7 +119,8 @@ public Mono<String> foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal pr
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
@@ -122,7 +128,7 @@ fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): Mono<
|
||||
return Mono.just(principal.getAttribute<Any>("sub").toString() + " is the subject")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Looking Up Attributes Via SpEL
|
||||
|
||||
@@ -130,8 +136,10 @@ Of course, this also means that attributes can be accessed via SpEL.
|
||||
|
||||
For example, if using `@EnableReactiveMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("principal?.attributes['sub'] = 'foo'")
|
||||
@@ -140,7 +148,8 @@ public Mono<String> forFoosEyesOnly() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("principal.attributes['sub'] = 'foo'")
|
||||
@@ -148,7 +157,7 @@ fun forFoosEyesOnly(): Mono<String> {
|
||||
return Mono.just("foo")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-sansboot]]
|
||||
== Overriding or Replacing Boot Auto Configuration
|
||||
@@ -158,8 +167,10 @@ There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf.
|
||||
The first is a `SecurityWebFilterChain` that configures the app as a resource server.
|
||||
When use Opaque Token, this `SecurityWebFilterChain` looks like:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -173,7 +184,8 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -188,15 +200,17 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
Replacing this is as simple as exposing the bean within the application:
|
||||
|
||||
.Replacing SecurityWebFilterChain
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -218,7 +232,8 @@ public class MyCustomSecurityConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -236,7 +251,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
@@ -244,8 +259,10 @@ Methods on the `oauth2ResourceServer` DSL will also override or replace auto con
|
||||
|
||||
For example, the second `@Bean` Spring Boot creates is a `ReactiveOpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -254,7 +271,8 @@ public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -262,7 +280,7 @@ fun introspector(): ReactiveOpaqueTokenIntrospector {
|
||||
return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If the application doesn't expose a `ReactiveOpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
@@ -273,8 +291,10 @@ And its configuration can be overridden using `introspectionUri()` and `introspe
|
||||
|
||||
An authorization server's Introspection Uri can be configured <<webflux-oauth2resourceserver-opaque-introspectionuri,as a configuration property>> or it can be supplied in the DSL:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -296,7 +316,8 @@ public class DirectlyConfiguredIntrospectionUri {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -314,7 +335,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Using `introspectionUri()` takes precedence over any configuration property.
|
||||
|
||||
@@ -323,8 +344,10 @@ Using `introspectionUri()` takes precedence over any configuration property.
|
||||
|
||||
More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `ReactiveOpaqueTokenIntrospector`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -345,7 +368,8 @@ public class DirectlyConfiguredIntrospector {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -362,7 +386,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This is handy when deeper configuration, like <<webflux-oauth2resourceserver-opaque-authorization-extraction,authority mapping>>or <<webflux-oauth2resourceserver-opaque-jwt-introspector,JWT revocation>> is necessary.
|
||||
|
||||
@@ -371,8 +395,10 @@ This is handy when deeper configuration, like <<webflux-oauth2resourceserver-opa
|
||||
|
||||
Or, exposing a `ReactiveOpaqueTokenIntrospector` `@Bean` has the same effect as `introspector()`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -381,7 +407,8 @@ public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -389,7 +416,7 @@ fun introspector(): ReactiveOpaqueTokenIntrospector {
|
||||
return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-authorization]]
|
||||
== Configuring Authorization
|
||||
@@ -402,8 +429,10 @@ When this is the case, Resource Server will attempt to coerce these scopes into
|
||||
|
||||
This means that to protect an endpoint or method with a scope derived from an Opaque Token, the corresponding expressions should include this prefix:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
@@ -422,7 +451,8 @@ public class MappedAuthorities {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -439,25 +469,28 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Or similarly with method security:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public Flux<Message> getMessages(...) {}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
fun getMessages(): Flux<Message> { }
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-authorization-extraction]]
|
||||
=== Extracting Authorities Manually
|
||||
@@ -478,8 +511,10 @@ Then Resource Server would generate an `Authentication` with two authorities, on
|
||||
|
||||
This can, of course, be customized using a custom `ReactiveOpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
@@ -501,7 +536,8 @@ public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueT
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
@@ -521,12 +557,14 @@ class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -535,7 +573,8 @@ public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -543,7 +582,7 @@ fun introspector(): ReactiveOpaqueTokenIntrospector {
|
||||
return CustomAuthoritiesOpaqueTokenIntrospector()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-jwt-introspector]]
|
||||
== Using Introspection with JWTs
|
||||
@@ -575,8 +614,10 @@ Now what?
|
||||
|
||||
In this case, you can create a custom `ReactiveOpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
@@ -602,7 +643,8 @@ public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospect
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class JwtOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
@@ -625,12 +667,14 @@ class JwtOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -639,7 +683,8 @@ public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -647,7 +692,7 @@ fun introspector(): ReactiveOpaqueTokenIntrospector {
|
||||
return JwtOpaqueTokenIntrospector()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-userinfo]]
|
||||
== Calling a `/userinfo` Endpoint
|
||||
@@ -663,8 +708,10 @@ This implementation below does three things:
|
||||
* Looks up the appropriate client registration associated with the `/userinfo` endpoint
|
||||
* Invokes and returns the response from the `/userinfo` endpoint
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
@@ -693,7 +740,8 @@ public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntro
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
@@ -716,13 +764,15 @@ class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If you aren't using `spring-security-oauth2-client`, it's still quite simple.
|
||||
You will simply need to invoke the `/userinfo` with your own instance of `WebClient`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
@@ -738,7 +788,8 @@ public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntro
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
@@ -751,12 +802,14 @@ class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Either way, having created your `ReactiveOpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -765,7 +818,8 @@ ReactiveOpaqueTokenIntrospector introspector() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -773,4 +827,4 @@ fun introspector(): ReactiveOpaqueTokenIntrospector {
|
||||
return UserInfoOpaqueTokenIntrospector()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
For example, we can test our example from xref:reactive/authorization/method.adoc#jc-erms[EnableReactiveMethodSecurity] using the same setup and annotations we did in xref:servlet/test/method.adoc#test-method[Testing Method Security].
|
||||
Here is a minimal sample of what we can do:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@@ -39,7 +41,8 @@ public class HelloWorldMessageServiceTests {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@@ -72,4 +75,4 @@ class HelloWorldMessageServiceTests {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
After xref:reactive/test/web/setup.adoc[applying the Spring Security support to `WebTestClient`] we can use either annotations or `mutateWith` support.
|
||||
For example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||
@@ -65,7 +67,8 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
@@ -112,6 +115,6 @@ fun messageWhenMutateWithMockAdminThenOk() {
|
||||
.expectBody<String>().isEqualTo("Hello World!")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In addition to `mockUser()`, Spring Security ships with several other convenience mutators for things like xref:reactive/test/web/csrf.adoc[CSRF] and xref:reactive/test/web/oauth2.adoc[OAuth 2.0].
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
Spring Security also provides support for CSRF testing with `WebTestClient`.
|
||||
For example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
@@ -17,7 +19,8 @@ this.rest
|
||||
...
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
|
||||
@@ -29,4 +32,4 @@ this.rest
|
||||
.uri("/login")
|
||||
...
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,10 @@
|
||||
|
||||
The basic setup looks like this:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
@@ -31,7 +33,8 @@ public class HelloWebfluxMethodApplicationTests {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity
|
||||
@@ -58,4 +61,4 @@ class HelloWebfluxMethodApplicationTests {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -108,12 +108,12 @@ If you are using other technologies which you aren't familiar with then you shou
|
||||
.. <<appendix-faq-session-listener-missing>>
|
||||
.. <<appendix-faq-unwanted-session-creation>>
|
||||
. Miscellaneous
|
||||
|
||||
.. <<appendix-faq-forbidden-csrf>>
|
||||
.. <<appendix-faq-no-security-on-forward>>
|
||||
.. <<appendix-faq-method-security-in-web-context>>
|
||||
.. <<appendix-faq-no-filters-no-context>>
|
||||
.. <<appendix-faq-method-security-with-taglib>>
|
||||
|
||||
[[appendix-faq-bad-credentials]]
|
||||
=== When I try to log in, I get an error message that says "Bad Credentials". What's wrong?
|
||||
|
||||
@@ -196,8 +196,10 @@ This will be different in different companies, so you have to find it out yourse
|
||||
Before adding a Spring Security LDAP configuration to an application, it's a good idea to write a simple test using standard Java LDAP code (without Spring Security involved), and make sure you can get that to work first.
|
||||
For example, to authenticate a user, you could use the following code:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
@@ -216,7 +218,8 @@ public void ldapAuthenticationIsSuccessful() throws Exception {
|
||||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@@ -230,7 +233,7 @@ fun ldapAuthenticationIsSuccessful() {
|
||||
val ctx = InitialLdapContext(env, null)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
=== Session Management
|
||||
|
||||
@@ -516,8 +519,10 @@ To load the data from an alternative source, you must be using an explicitly dec
|
||||
You can't use the namespace.
|
||||
You would then implement `FilterInvocationSecurityMetadataSource` to load the data as you please for a particular `FilterInvocation` footnote:[The `FilterInvocation` object contains the `HttpServletRequest`, so you can obtain the URL or any other relevant information on which to base your decision on what the list of returned attributes will contain.]. A very basic outline would look something like this:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
@@ -546,7 +551,8 @@ You would then implement `FilterInvocationSecurityMetadataSource` to load the da
|
||||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class MyFilterSecurityMetadataSource : FilterInvocationSecurityMetadataSource {
|
||||
@@ -569,7 +575,7 @@ class MyFilterSecurityMetadataSource : FilterInvocationSecurityMetadataSource {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
For more information, look at the code for `DefaultFilterInvocationSecurityMetadataSource`.
|
||||
|
||||
@@ -582,8 +588,10 @@ The `DefaultLdapAuthoritiesPopulator` loads the user authorities from the LDAP d
|
||||
|
||||
To use JDBC instead, you can implement the interface yourself, using whatever SQL is appropriate for your schema:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
@@ -609,7 +617,8 @@ To use JDBC instead, you can implement the interface yourself, using whatever SQ
|
||||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
|
||||
@@ -629,7 +638,7 @@ class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You would then add a bean of this type to your application context and inject it into the `LdapAuthenticationProvider`. This is covered in the section on configuring LDAP using explicit Spring beans in the LDAP chapter of the reference manual.
|
||||
Note that you can't use the namespace for configuration in this case.
|
||||
@@ -647,8 +656,10 @@ More information can be found in the https://docs.spring.io/spring/docs/3.0.x/sp
|
||||
Normally, you would add the functionality you require to the `postProcessBeforeInitialization` method of `BeanPostProcessor`. Let's say that you want to customize the `AuthenticationDetailsSource` used by the `UsernamePasswordAuthenticationFilter`, (created by the `form-login` element). You want to extract a particular header called `CUSTOM_HEADER` from the request and make use of it while authenticating the user.
|
||||
The processor class would look like this:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
@@ -674,7 +685,8 @@ public class CustomBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class CustomBeanPostProcessor : BeanPostProcessor {
|
||||
@@ -692,7 +704,7 @@ class CustomBeanPostProcessor : BeanPostProcessor {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You would then register this bean in your application context.
|
||||
Spring will automatically invoke it on the beans defined in the application context.
|
||||
|
||||
@@ -28,8 +28,10 @@ In this instance the `Filter` will typically write the `HttpServletResponse`.
|
||||
The power of the `Filter` comes from the `FilterChain` that is passed into it.
|
||||
|
||||
.`FilterChain` Usage Example
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
||||
@@ -39,7 +41,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
@@ -48,7 +51,7 @@ fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterCh
|
||||
// do something after the rest of the application
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Since a `Filter` only impacts downstream ``Filter``s and the `Servlet`, the order each `Filter` is invoked is extremely important.
|
||||
|
||||
@@ -70,8 +73,10 @@ image::{figures}/delegatingfilterproxy.png[]
|
||||
The pseudo code of `DelegatingFilterProxy` can be seen below.
|
||||
|
||||
.`DelegatingFilterProxy` Pseudo Code
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",subs="+quotes,+macros"]
|
||||
----
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
||||
@@ -83,7 +88,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",subs="+quotes,+macros"]
|
||||
----
|
||||
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
@@ -94,7 +100,7 @@ fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterCh
|
||||
delegate.doFilter(request, response)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Another benefit of `DelegatingFilterProxy` is that it allows delaying looking `Filter` bean instances.
|
||||
This is important because the container needs to register the `Filter` instances before the container can startup.
|
||||
@@ -158,46 +164,224 @@ In fact, a `SecurityFilterChain` might have zero security ``Filter``s if the app
|
||||
[[servlet-security-filters]]
|
||||
== Security Filters
|
||||
|
||||
Spring Security uses a number of Servlet Filters (https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.pdf[Jakarta Servlet Spec, Chapter 6]) to provide security to your application.
|
||||
The Security Filters are inserted into the <<servlet-filterchainproxy>> with the <<servlet-securityfilterchain>> API.
|
||||
The <<servlet-filters-review,order of ``Filter``>>s matters.
|
||||
Those filters can be used for a number of different purposes, like xref:servlet/authentication/index.adoc[authentication], xref:servlet/authorization/index.adoc[authorization], xref:servlet/exploits/index.adoc[exploit protection], and more.
|
||||
The filters are executed in a specific order to guarantee that they are invoked at the right time, for example, the `Filter` that performs authentication should be invoked before the `Filter` that performs authorization.
|
||||
It is typically not necessary to know the ordering of Spring Security's ``Filter``s.
|
||||
However, there are times that it is beneficial to know the ordering
|
||||
However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the {gh-url}/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java[`FilterOrderRegistration` code].
|
||||
|
||||
Below is a comprehensive list of Spring Security Filter ordering:
|
||||
To exemplify the above paragraph, let's consider the following security configuration:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(Customizer.withDefaults())
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.formLogin(Customizer.withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.web.servlet.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
csrf { }
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
httpBasic { }
|
||||
formLogin { }
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The above configuration will result in the following `Filter` ordering:
|
||||
|
||||
[cols="1,1", options="header"]
|
||||
|====
|
||||
| Filter | Added by
|
||||
| xref:servlet/exploits/csrf.adoc[CsrfFilter] | `HttpSecurity#csrf`
|
||||
| xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[UsernamePasswordAuthenticationFilter] | `HttpSecurity#formLogin`
|
||||
| xref:servlet/authentication/passwords/basic.adoc[BasicAuthenticationFilter] | `HttpSecurity#httpBasic`
|
||||
| xref:servlet/authorization/authorize-http-requests.adoc[AuthorizationFilter] | `HttpSecurity#authorizeHttpRequests`
|
||||
|====
|
||||
|
||||
1. First, the `CsrfFilter` is invoked to protect against xref:servlet/exploits/csrf.adoc[CSRF attacks].
|
||||
2. Second, the authentication filters are invoked to authenticate the request.
|
||||
3. Third, the `AuthorizationFilter` is invoked to authorize the request.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
There might be other `Filter` instances that are not listed above.
|
||||
If you want to see the list of filters invoked for a particular request, you can <<servlet-print-filters,print them>>.
|
||||
====
|
||||
|
||||
[[servlet-print-filters]]
|
||||
=== Printing the Security Filters
|
||||
|
||||
Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request.
|
||||
For example, you want to make sure that the <<adding-custom-filter,filter you have added>> is in the list of the security filters.
|
||||
|
||||
The list of filters is printed at INFO level on the application startup, so you can see something like the following on the console output for example:
|
||||
|
||||
[source,text,role="terminal"]
|
||||
----
|
||||
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
|
||||
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
|
||||
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
|
||||
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
|
||||
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
|
||||
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
|
||||
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
|
||||
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
|
||||
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
|
||||
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
|
||||
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
|
||||
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
|
||||
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
|
||||
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
|
||||
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
|
||||
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
|
||||
----
|
||||
|
||||
And that will give a pretty good idea of the security filters that are configured for <<servlet-securityfilterchain,each filter chain>>.
|
||||
|
||||
But that is not all, you can also configure your application to print the invocation of each individual filter for each request.
|
||||
That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from.
|
||||
To do that, you can configure your application to <<servlet-logging,log the security events>>.
|
||||
|
||||
[[adding-custom-filter]]
|
||||
=== Adding a Custom Filter to the Filter Chain
|
||||
|
||||
Mostly of the times, the default security filters are enough to provide security to your application.
|
||||
However, there might be times that you want to add a custom `Filter` to the security filter chain.
|
||||
|
||||
For example, let's say that you want to add a `Filter` that gets a tenant id header and check if the current user has access to that tenant.
|
||||
The previous description already gives us a clue on where to add the filter, since we need to know the current user, we need to add it after the authentication filters.
|
||||
|
||||
First, let's create the `Filter`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
||||
public class TenantFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
|
||||
String tenantId = request.getHeader("X-Tenant-Id"); <1>
|
||||
boolean hasAccess = isUserAllowed(tenantId); <2>
|
||||
if (hasAccess) {
|
||||
filterChain.doFilter(request, response); <3>
|
||||
return;
|
||||
}
|
||||
throw new AccessDeniedException("Access denied"); <4>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
The sample code above does the following:
|
||||
|
||||
<1> Get the tenant id from the request header.
|
||||
<2> Check if the current user has access to the tenant id.
|
||||
<3> If the user has access, then invoke the rest of the filters in the chain.
|
||||
<4> If the user does not have access, then throw an `AccessDeniedException`.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Instead of implementing `Filter`, you can extend from {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter] which is a base class for filters that are only invoked once per request and provides a `doFilterInternal` method with the `HttpServletRequest` and `HttpServletResponse` parameters.
|
||||
====
|
||||
|
||||
Now, we need to add the filter to the security filter chain.
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class); <1>
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
// ...
|
||||
.addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) <1>
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
<1> Use `HttpSecurity#addFilterBefore` to add the `TenantFilter` before the `AuthorizationFilter`.
|
||||
|
||||
By adding the filter before the `AuthorizationFilter` we are making sure that the `TenantFilter` is invoked after the authentication filters.
|
||||
You can also use `HttpSecurity#addFilterAfter` to add the filter after a particular filter or `HttpSecurity#addFilterAt` to add the filter at a particular filter position in the filter chain.
|
||||
|
||||
And that's it, now the `TenantFilter` will be invoked in the filter chain and will check if the current user has access to the tenant id.
|
||||
|
||||
Be careful when you declare your filter as a Spring bean, either by annotating it with `@Component` or by declaring it as a bean in your configuration, because Spring Boot will automatically {spring-boot-reference-url}web.html#web.servlet.embedded-container.servlets-filters-listeners.beans[register it with the embedded container].
|
||||
That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.
|
||||
|
||||
If you still want to declare your filter as a Spring bean to take advantage of dependency injection for example, and avoid the duplicate invocation, you can tell Spring Boot to not register it with the container by declaring a `FilterRegistrationBean` bean and setting its `enabled` property to `false`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
|
||||
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
|
||||
registration.setEnabled(false);
|
||||
return registration;
|
||||
}
|
||||
----
|
||||
|
||||
* xref:servlet/authentication/session-management.adoc#session-mgmt-force-session-creation[`ForceEagerSessionCreationFilter`]
|
||||
* ChannelProcessingFilter
|
||||
* WebAsyncManagerIntegrationFilter
|
||||
* SecurityContextPersistenceFilter
|
||||
* HeaderWriterFilter
|
||||
* CorsFilter
|
||||
* CsrfFilter
|
||||
* LogoutFilter
|
||||
* OAuth2AuthorizationRequestRedirectFilter
|
||||
* Saml2WebSsoAuthenticationRequestFilter
|
||||
* X509AuthenticationFilter
|
||||
* AbstractPreAuthenticatedProcessingFilter
|
||||
* CasAuthenticationFilter
|
||||
* OAuth2LoginAuthenticationFilter
|
||||
* Saml2WebSsoAuthenticationFilter
|
||||
* xref:servlet/authentication/passwords/form.adoc#servlet-authentication-usernamepasswordauthenticationfilter[`UsernamePasswordAuthenticationFilter`]
|
||||
* OpenIDAuthenticationFilter
|
||||
* DefaultLoginPageGeneratingFilter
|
||||
* DefaultLogoutPageGeneratingFilter
|
||||
* ConcurrentSessionFilter
|
||||
* xref:servlet/authentication/passwords/digest.adoc#servlet-authentication-digest[`DigestAuthenticationFilter`]
|
||||
* BearerTokenAuthenticationFilter
|
||||
* xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[`BasicAuthenticationFilter`]
|
||||
* <<requestcacheawarefilter,RequestCacheAwareFilter>>
|
||||
* SecurityContextHolderAwareRequestFilter
|
||||
* JaasApiIntegrationFilter
|
||||
* RememberMeAuthenticationFilter
|
||||
* AnonymousAuthenticationFilter
|
||||
* OAuth2AuthorizationCodeGrantFilter
|
||||
* SessionManagementFilter
|
||||
* <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>>
|
||||
* xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]
|
||||
* SwitchUserFilter
|
||||
|
||||
[[servlet-exceptiontranslationfilter]]
|
||||
== Handling Security Exceptions
|
||||
@@ -275,8 +459,10 @@ Or you may want to shut off this feature since you always want to redirect the u
|
||||
To do that, you can use {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[the `NullRequestCache` implementation].
|
||||
|
||||
.Prevent the Request From Being Saved
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -291,7 +477,8 @@ SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -306,7 +493,8 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http auto-config="true">
|
||||
@@ -316,10 +504,59 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
|
||||
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
[[requestcacheawarefilter]]
|
||||
=== 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>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -108,8 +108,10 @@ https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc
|
||||
|
||||
This means that a construct like this one:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -122,7 +124,8 @@ public String method(Authentication authentication) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -134,7 +137,7 @@ fun method(authentication: Authentication?): String {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
will always return "not anonymous", even for anonymous requests.
|
||||
The reason is that Spring MVC resolves the parameter using `HttpServletRequest#getPrincipal`, which is `null` when the request is anonymous.
|
||||
@@ -142,8 +145,10 @@ The reason is that Spring MVC resolves the parameter using `HttpServletRequest#g
|
||||
If you'd like to obtain the `Authentication` in anonymous requests, use `@CurrentSecurityContext` instead:
|
||||
|
||||
.Use CurrentSecurityContext for Anonymous requests
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
@@ -152,11 +157,12 @@ public String method(@CurrentSecurityContext SecurityContext context) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
fun method(@CurrentSecurityContext context : SecurityContext) : String =
|
||||
context!!.authentication!!.name
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -31,8 +31,10 @@ If it contains a value, then it is used as the currently authenticated user.
|
||||
The simplest way to indicate a user is authenticated is to set the `SecurityContextHolder` directly.
|
||||
|
||||
.Setting `SecurityContextHolder`
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext(); // <1>
|
||||
@@ -43,7 +45,8 @@ context.setAuthentication(authentication);
|
||||
SecurityContextHolder.setContext(context); // <3>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val context: SecurityContext = SecurityContextHolder.createEmptyContext() // <1>
|
||||
@@ -52,7 +55,7 @@ context.authentication = authentication
|
||||
|
||||
SecurityContextHolder.setContext(context) // <3>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
<1> We start by creating an empty `SecurityContext`.
|
||||
It is important to create a new `SecurityContext` instance instead of using `SecurityContextHolder.getContext().setAuthentication(authentication)` to avoid race conditions across multiple threads.
|
||||
@@ -66,8 +69,10 @@ Spring Security will use this information for xref:servlet/authorization/index.a
|
||||
If you wish to obtain information about the authenticated principal, you can do so by accessing the `SecurityContextHolder`.
|
||||
|
||||
.Access Currently Authenticated User
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
@@ -77,7 +82,8 @@ Object principal = authentication.getPrincipal();
|
||||
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val context = SecurityContextHolder.getContext()
|
||||
@@ -86,7 +92,7 @@ val username = authentication.name
|
||||
val principal = authentication.principal
|
||||
val authorities = authentication.authorities
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
// FIXME: add links to HttpServletRequest.getRemoteUser() and @CurrentSecurityContext @AuthenticationPrincipal
|
||||
|
||||
|
||||
@@ -340,8 +340,10 @@ Now that Spring Security obtains PGTs, you can use them to create proxy tickets
|
||||
The CAS xref:samples.adoc#samples[sample application] contains a working example in the `ProxyTicketSampleServlet`.
|
||||
Example code can be found below:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response)
|
||||
@@ -360,7 +362,8 @@ String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) {
|
||||
@@ -376,7 +379,7 @@ protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?)
|
||||
val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[cas-pt]]
|
||||
=== Proxy Ticket Authentication
|
||||
|
||||
@@ -6,8 +6,10 @@ For each authentication that succeeds or fails, a `AuthenticationSuccessEvent` o
|
||||
To listen for these events, you must first publish an `AuthenticationEventPublisher`.
|
||||
Spring Security's `DefaultAuthenticationEventPublisher` will probably do fine:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -17,7 +19,8 @@ public AuthenticationEventPublisher authenticationEventPublisher
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -26,12 +29,14 @@ fun authenticationEventPublisher
|
||||
return DefaultAuthenticationEventPublisher(applicationEventPublisher)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Then, you can use Spring's `@EventListener` support:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
@@ -48,7 +53,8 @@ public class AuthenticationEvents {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
@@ -64,7 +70,7 @@ class AuthenticationEvents {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
While similar to `AuthenticationSuccessHandler` and `AuthenticationFailureHandler`, these are nice in that they can be used independently from the servlet API.
|
||||
|
||||
@@ -89,8 +95,10 @@ The publisher does an exact `Exception` match, which means that sub-classes of t
|
||||
|
||||
To that end, you may want to supply additional mappings to the publisher via the `setAdditionalExceptionMappings` method:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -106,7 +114,8 @@ public AuthenticationEventPublisher authenticationEventPublisher
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -119,14 +128,16 @@ fun authenticationEventPublisher
|
||||
return authenticationEventPublisher
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Default Event
|
||||
|
||||
And, you can supply a catch-all event to fire in the case of any `AuthenticationException`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -140,7 +151,8 @@ public AuthenticationEventPublisher authenticationEventPublisher
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -151,4 +163,4 @@ fun authenticationEventPublisher
|
||||
return authenticationEventPublisher
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -16,8 +16,10 @@ The default is that accessing the URL `/logout` will log the user out by:
|
||||
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
|
||||
|
||||
.Logout Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
@@ -34,7 +36,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
-----
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
@@ -51,7 +54,7 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
// ...
|
||||
}
|
||||
-----
|
||||
====
|
||||
======
|
||||
|
||||
<1> Provides logout support.
|
||||
<2> The URL that triggers log out to occur (default is `/logout`).
|
||||
|
||||
@@ -58,9 +58,11 @@ However, as soon as any servlet based configuration is provided, HTTP Basic must
|
||||
A minimal, explicit configuration can be found below:
|
||||
|
||||
.Explicit HTTP Basic Configuration
|
||||
====
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
@@ -71,8 +73,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
.XML
|
||||
----
|
||||
<http>
|
||||
<!-- ... -->
|
||||
@@ -80,8 +83,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
</http>
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Bean
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
@@ -92,4 +96,4 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -25,21 +25,21 @@ This is a value the server generates.
|
||||
Spring Security's nonce adopts the following format:
|
||||
|
||||
.Digest Syntax
|
||||
====
|
||||
[source,txt]
|
||||
----
|
||||
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
|
||||
expirationTime: The date and time when the nonce expires, expressed in milliseconds
|
||||
key: A private key to prevent modification of the nonce token
|
||||
----
|
||||
====
|
||||
|
||||
You will need to ensure you xref:features/authentication/password-storage.adoc#authentication-password-storage-configuration[configure] insecure plain text xref:features/authentication/password-storage.adoc#authentication-password-storage[Password Storage] using `NoOpPasswordEncoder`.
|
||||
The following provides an example of configuring Digest Authentication with Java Configuration:
|
||||
|
||||
.Digest Authentication
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Autowired
|
||||
@@ -67,7 +67,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="digestFilter"
|
||||
@@ -87,4 +88,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
<custom-filter ref="userFilter" position="DIGEST_AUTH_FILTER"/>
|
||||
</http>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -67,8 +67,10 @@ However, as soon as any servlet based configuration is provided, form based log
|
||||
A minimal, explicit Java configuration can be found below:
|
||||
|
||||
.Form Log In
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
@@ -78,7 +80,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -87,7 +90,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
</http>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
@@ -97,7 +101,7 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In this configuration Spring Security will render a default log in page.
|
||||
Most production applications will require a custom log in form.
|
||||
@@ -106,8 +110,10 @@ Most production applications will require a custom log in form.
|
||||
The configuration below demonstrates how to provide a custom log in form.
|
||||
|
||||
.Custom Log In Form Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
@@ -120,7 +126,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
@@ -130,7 +137,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
</http>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
@@ -143,16 +151,14 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-authentication-form-custom-html]]
|
||||
When the login page is specified in the Spring Security configuration, you are responsible for rendering the page.
|
||||
// FIXME: default login page rendered by Spring Security
|
||||
Below is a https://www.thymeleaf.org/[Thymeleaf] template that produces an HTML login form that complies with a login page of `/login`:
|
||||
|
||||
.Log In Form
|
||||
====
|
||||
.src/main/resources/templates/login.html
|
||||
.Log In Form - src/main/resources/templates/login.html
|
||||
[source,xml]
|
||||
----
|
||||
<!DOCTYPE html>
|
||||
@@ -178,7 +184,6 @@ Below is a https://www.thymeleaf.org/[Thymeleaf] template that produces an HTML
|
||||
</body>
|
||||
</html>
|
||||
----
|
||||
====
|
||||
|
||||
There are a few key points about the default HTML form:
|
||||
|
||||
@@ -197,8 +202,10 @@ If you are using Spring MVC, you will need a controller that maps `GET /login` t
|
||||
A minimal sample `LoginController` can be seen below:
|
||||
|
||||
.LoginController
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -210,7 +217,8 @@ class LoginController {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
@@ -221,4 +229,4 @@ class LoginController {
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -8,8 +8,10 @@ Spring Security's `InMemoryUserDetailsManager` implements xref:servlet/authentic
|
||||
In this sample we use xref:features/authentication/password-storage.adoc#authentication-password-storage-boot-cli[Spring Boot CLI] to encode the password of `password` and get the encoded password of `+{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW+`.
|
||||
|
||||
.InMemoryUserDetailsManager Java Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -28,7 +30,8 @@ public UserDetailsService users() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<user-service>
|
||||
@@ -41,7 +44,8 @@ public UserDetailsService users() {
|
||||
</user-service>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -59,7 +63,7 @@ fun users(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(user, admin)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
The samples above store the passwords in a secure format, but leave a lot to be desired in terms of getting started experience.
|
||||
|
||||
@@ -69,8 +73,10 @@ However, it does not protect against obtaining the password by decompiling the s
|
||||
For this reason, `User.withDefaultPasswordEncoder` should only be used for "getting started" and is not intended for production.
|
||||
|
||||
.InMemoryUserDetailsManager with User.withDefaultPasswordEncoder
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -91,7 +97,8 @@ public UserDetailsService users() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -111,13 +118,12 @@ fun users(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(user, admin)
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
There is no simple way to use `User.withDefaultPasswordEncoder` with XML based configuration.
|
||||
For demos or just getting started, you can choose to prefix the password with `+{noop}+` to indicate xref:features/authentication/password-storage.adoc#authentication-password-storage-dpe-format[no encoding should be used].
|
||||
|
||||
.<user-service> `+{noop}+` XML Configuration
|
||||
====
|
||||
[source,xml,attrs="-attributes"]
|
||||
----
|
||||
<user-service>
|
||||
@@ -129,4 +135,3 @@ For demos or just getting started, you can choose to prefix the password with `+
|
||||
authorities="ROLE_USER,ROLE_ADMIN" />
|
||||
</user-service>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -30,7 +30,6 @@ The default schema is also exposed as a classpath resource named `org/springfram
|
||||
====
|
||||
|
||||
.Default User Schema
|
||||
====
|
||||
[source,sql]
|
||||
----
|
||||
create table users(
|
||||
@@ -46,13 +45,11 @@ create table authorities (
|
||||
);
|
||||
create unique index ix_auth_username on authorities (username,authority);
|
||||
----
|
||||
====
|
||||
|
||||
Oracle is a popular database choice, but requires a slightly different schema.
|
||||
You can find the default Oracle Schema for users below.
|
||||
|
||||
.Default User Schema for Oracle Databases
|
||||
====
|
||||
[source,sql]
|
||||
----
|
||||
CREATE TABLE USERS (
|
||||
@@ -69,7 +66,6 @@ CREATE TABLE AUTHORITIES (
|
||||
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
|
||||
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-authentication-jdbc-schema-group]]
|
||||
=== Group Schema
|
||||
@@ -78,7 +74,6 @@ If your application is leveraging groups, you will need to provide the groups sc
|
||||
The default schema for groups can be found below.
|
||||
|
||||
.Default Group Schema
|
||||
====
|
||||
[source,sql]
|
||||
----
|
||||
create table groups (
|
||||
@@ -99,7 +94,6 @@ create table group_members (
|
||||
constraint fk_group_members_group foreign key(group_id) references groups(id)
|
||||
);
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-authentication-jdbc-datasource]]
|
||||
== Setting up a DataSource
|
||||
@@ -108,8 +102,10 @@ Before we configure `JdbcUserDetailsManager`, we must create a `DataSource`.
|
||||
In our example, we will setup an https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#jdbc-embedded-database-support[embedded DataSource] that is initialized with the <<servlet-authentication-jdbc-schema,default user schema>>.
|
||||
|
||||
.Embedded Data Source
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -121,7 +117,8 @@ DataSource dataSource() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<jdbc:embedded-database>
|
||||
@@ -129,7 +126,8 @@ DataSource dataSource() {
|
||||
</jdbc:embedded-database>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -140,7 +138,7 @@ fun dataSource(): DataSource {
|
||||
.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
In a production environment, you will want to ensure you setup a connection to an external database.
|
||||
|
||||
@@ -151,9 +149,11 @@ In this sample we use xref:features/authentication/password-storage.adoc#authent
|
||||
See the xref:features/authentication/password-storage.adoc#authentication-password-storage[PasswordEncoder] section for more details about how to store passwords.
|
||||
|
||||
.JdbcUserDetailsManager
|
||||
====
|
||||
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -175,7 +175,8 @@ UserDetailsManager users(DataSource dataSource) {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<jdbc-user-service>
|
||||
@@ -188,7 +189,8 @@ UserDetailsManager users(DataSource dataSource) {
|
||||
</jdbc-user-service>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -209,4 +211,4 @@ fun users(dataSource: DataSource): UserDetailsManager {
|
||||
return users
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -97,8 +97,10 @@ uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
If you wish to use https://ldap.com/unboundid-ldap-sdk-for-java/[UnboundID], then specify the following dependencies:
|
||||
|
||||
.UnboundID Dependencies
|
||||
====
|
||||
.Maven
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary",subs="verbatim,attributes"]
|
||||
----
|
||||
<dependency>
|
||||
@@ -109,21 +111,24 @@ If you wish to use https://ldap.com/unboundid-ldap-sdk-for-java/[UnboundID], the
|
||||
</dependency>
|
||||
----
|
||||
|
||||
.Gradle
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary",subs="verbatim,attributes"]
|
||||
----
|
||||
depenendencies {
|
||||
runtimeOnly "com.unboundid:unboundid-ldapsdk:{unboundid-ldapsdk-version}"
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You can then configure the Embedded LDAP Server using an `EmbeddedLdapServerContextSourceFactoryBean`.
|
||||
This will instruct Spring Security to start an in-memory LDAP server.
|
||||
|
||||
.Embedded LDAP Server Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -132,7 +137,8 @@ public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -140,14 +146,16 @@ fun contextSourceFactoryBean(): EmbeddedLdapServerContextSourceFactoryBean {
|
||||
return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Alternatively, you can manually configure the Embedded LDAP Server.
|
||||
If you choose this approach, you will be responsible for managing the lifecycle of the Embedded LDAP Server.
|
||||
|
||||
.Explicit Embedded LDAP Server Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -157,7 +165,8 @@ UnboundIdContainer ldapContainer() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean class="org.springframework.security.ldap.server.UnboundIdContainer"
|
||||
@@ -165,7 +174,8 @@ UnboundIdContainer ldapContainer() {
|
||||
c:ldif="classpath:users.ldif"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -173,7 +183,7 @@ fun ldapContainer(): UnboundIdContainer {
|
||||
return UnboundIdContainer("dc=springframework,dc=org","classpath:users.ldif")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-apacheds]]
|
||||
=== Embedded ApacheDS Server
|
||||
@@ -188,8 +198,10 @@ Once a stable release of ApacheDS 2.x is available, we will consider updating.
|
||||
If you wish to use https://directory.apache.org/apacheds/[Apache DS], then specify the following dependencies:
|
||||
|
||||
.ApacheDS Dependencies
|
||||
====
|
||||
.Maven
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary",subs="+attributes"]
|
||||
----
|
||||
<dependency>
|
||||
@@ -206,7 +218,8 @@ If you wish to use https://directory.apache.org/apacheds/[Apache DS], then speci
|
||||
</dependency>
|
||||
----
|
||||
|
||||
.Gradle
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary",subs="+attributes"]
|
||||
----
|
||||
depenendencies {
|
||||
@@ -214,13 +227,15 @@ depenendencies {
|
||||
runtimeOnly "org.apache.directory.server:apacheds-server-jndi:{apacheds-core-version}"
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
You can then configure the Embedded LDAP Server
|
||||
|
||||
.Embedded LDAP Server Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -230,7 +245,8 @@ ApacheDSContainer ldapContainer() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean class="org.springframework.security.ldap.server.ApacheDSContainer"
|
||||
@@ -238,7 +254,8 @@ ApacheDSContainer ldapContainer() {
|
||||
c:ldif="classpath:users.ldif"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -246,7 +263,7 @@ fun ldapContainer(): ApacheDSContainer {
|
||||
return ApacheDSContainer("dc=springframework,dc=org", "classpath:users.ldif")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-contextsource]]
|
||||
== LDAP ContextSource
|
||||
@@ -256,8 +273,10 @@ This is done by creating an LDAP `ContextSource`, which is the equivalent of a J
|
||||
If you have already configured an `EmbeddedLdapServerContextSourceFactoryBean`, Spring Security will create an LDAP `ContextSource` that points to the embedded LDAP server.
|
||||
|
||||
.LDAP Context Source with Embedded LDAP Server
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -269,7 +288,8 @@ public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -279,13 +299,15 @@ fun contextSourceFactoryBean(): EmbeddedLdapServerContextSourceFactoryBean {
|
||||
return contextSourceFactoryBean
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
Alternatively, you can explicitly configure the LDAP `ContextSource` to connect to the supplied LDAP server.
|
||||
|
||||
.LDAP Context Source
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
ContextSource contextSource(UnboundIdContainer container) {
|
||||
@@ -293,21 +315,23 @@ ContextSource contextSource(UnboundIdContainer container) {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<ldap-server
|
||||
url="ldap://localhost:53389/dc=springframework,dc=org" />
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
fun contextSource(container: UnboundIdContainer): ContextSource {
|
||||
return DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-authentication]]
|
||||
== Authentication
|
||||
@@ -336,8 +360,10 @@ The advantage to using bind authentication is that the user's secrets (i.e. pass
|
||||
An example of bind authentication configuration can be found below.
|
||||
|
||||
.Bind Authentication
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -348,14 +374,16 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<ldap-authentication-provider
|
||||
user-dn-pattern="uid={0},ou=people"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -365,15 +393,17 @@ fun authenticationManager(contextSource: BaseLdapPathContextSource): Authenticat
|
||||
return factory.createAuthenticationManager()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
This simple example would obtain the DN for the user by substituting the user login name in the supplied pattern and attempting to bind as that user with the login password.
|
||||
This is OK if all your users are stored under a single node in the directory.
|
||||
If instead you wished to configure an LDAP search filter to locate the user, you could use the following:
|
||||
|
||||
.Bind Authentication with Search Filter
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -385,7 +415,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<ldap-authentication-provider
|
||||
@@ -393,7 +424,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
user-search-base="ou=people"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -404,7 +436,7 @@ fun authenticationManager(contextSource: BaseLdapPathContextSource): Authenticat
|
||||
return factory.createAuthenticationManager()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
If used with the `ContextSource` <<servlet-authentication-ldap-contextsource,definition above>>, this would perform a search under the DN `ou=people,dc=springframework,dc=org` using `+(uid={0})+` as a filter.
|
||||
Again the user login name is substituted for the parameter in the filter name, so it will search for an entry with the `uid` attribute equal to the user name.
|
||||
@@ -418,8 +450,10 @@ This can either be done by retrieving the value of the password attribute and ch
|
||||
An LDAP compare cannot be done when the password is properly hashed with a random salt.
|
||||
|
||||
.Minimal Password Compare Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -431,7 +465,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<ldap-authentication-provider
|
||||
@@ -440,7 +475,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
</ldap-authentication-provider>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -452,13 +488,15 @@ fun authenticationManager(contextSource: BaseLdapPathContextSource?): Authentica
|
||||
return factory.createAuthenticationManager()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
A more advanced configuration with some customizations can be found below.
|
||||
|
||||
.Password Compare Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -471,7 +509,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<ldap-authentication-provider
|
||||
@@ -484,7 +523,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -497,7 +537,7 @@ fun authenticationManager(contextSource: BaseLdapPathContextSource): Authenticat
|
||||
return factory.createAuthenticationManager()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
<1> Specify the password attribute as `pwd`
|
||||
|
||||
@@ -506,8 +546,10 @@ fun authenticationManager(contextSource: BaseLdapPathContextSource): Authenticat
|
||||
Spring Security's `LdapAuthoritiesPopulator` is used to determine what authorites are returned for the user.
|
||||
|
||||
.LdapAuthoritiesPopulator Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -528,7 +570,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<ldap-authentication-provider
|
||||
@@ -536,7 +579,8 @@ AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSou
|
||||
group-search-filter="member={0}"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
@Bean
|
||||
@@ -557,7 +601,7 @@ fun authenticationManager(
|
||||
return factory.createAuthenticationManager()
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
== Active Directory
|
||||
|
||||
@@ -571,8 +615,10 @@ This is not currently supported, but hopefully will be in a future version.].
|
||||
An example configuration can be seen below:
|
||||
|
||||
.Example Active Directory Configuration
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -581,7 +627,8 @@ ActiveDirectoryLdapAuthenticationProvider authenticationProvider() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<bean id="authenticationProvider"
|
||||
@@ -591,7 +638,8 @@ ActiveDirectoryLdapAuthenticationProvider authenticationProvider() {
|
||||
</bean>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -599,4 +647,4 @@ fun authenticationProvider(): ActiveDirectoryLdapAuthenticationProvider {
|
||||
return ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/")
|
||||
}
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
@@ -10,8 +10,10 @@ For example, the following will customize authentication assuming that `CustomUs
|
||||
NOTE: This is only used if the `AuthenticationManagerBuilder` has not been populated and no `AuthenticationProviderBean` is defined.
|
||||
|
||||
.Custom UserDetailsService Bean
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -20,18 +22,20 @@ CustomUserDetailsService customUserDetailsService() {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
<b:bean class="example.CustomUserDetailsService"/>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun customUserDetailsService() = CustomUserDetailsService()
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
// FIXME: Add CustomUserDetails example with links to @AuthenticationPrincipal
|
||||
|
||||
@@ -25,7 +25,6 @@ Location: /login
|
||||
The user submits their username and password.
|
||||
|
||||
.Username and Password Submitted
|
||||
====
|
||||
[source,http]
|
||||
----
|
||||
POST /login HTTP/1.1
|
||||
@@ -34,31 +33,26 @@ Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
|
||||
|
||||
username=user&password=password&_csrf=35942e65-a172-4cd4-a1d4-d16a51147b3e
|
||||
----
|
||||
====
|
||||
|
||||
Upon authenticating the user, the user is associated to a new session id to prevent xref:servlet/authentication/session-management.adoc#ns-session-fixation[session fixation attacks].
|
||||
|
||||
.Authenticated User is Associated to New Session
|
||||
====
|
||||
[source,http]
|
||||
----
|
||||
HTTP/1.1 302 Found
|
||||
Location: /
|
||||
Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax
|
||||
----
|
||||
====
|
||||
|
||||
Subsequent requests include the session cookie which is used to authenticate the user for the remainder of the session.
|
||||
|
||||
.Authenticated Session Provided as Credentials
|
||||
====
|
||||
[source,http]
|
||||
----
|
||||
GET / HTTP/1.1
|
||||
Host: example.com
|
||||
Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
[[securitycontextrepository]]
|
||||
@@ -81,7 +75,7 @@ If it is not desirable to associate the `SecurityContext` to an `HttpSession` (i
|
||||
[[requestattributesecuritycontextrepository]]
|
||||
=== RequestAttributeSecurityContextRepository
|
||||
|
||||
The {security-api-url}org/springframework/security/web/context/RequestAttributeSecurityContextRepository.html[`RequestAttributeSecurityContextRepository`] saves the `SecurityContext` as a request attribute to make sure the `SecurityContext` is avaible for a single request that occurs across dispatch types that may clear out the `SecurityContext`.
|
||||
The {security-api-url}org/springframework/security/web/context/RequestAttributeSecurityContextRepository.html[`RequestAttributeSecurityContextRepository`] saves the `SecurityContext` as a request attribute to make sure the `SecurityContext` is available for a single request that occurs across dispatch types that may clear out the `SecurityContext`.
|
||||
|
||||
For example, assume that a client makes a request, is authenticated, and then an error occurs.
|
||||
Depending on the servlet container implementation, the error means that any `SecurityContext` that was established is cleared out and then the error dispatch is made.
|
||||
@@ -89,8 +83,10 @@ When the error dispatch is made, there is no `SecurityContext` established.
|
||||
This means that the error page cannot use the `SecurityContext` for authorization or displaying the current user unless the `SecurityContext` is persisted somehow.
|
||||
|
||||
.Use RequestAttributeSecurityContextRepository
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
@@ -103,7 +99,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http security-context-repository-ref="contextRepository">
|
||||
@@ -112,7 +109,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
<b:bean name="contextRepository"
|
||||
class="org.springframework.security.web.context.RequestAttributeSecurityContextRepository" />
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[[delegatingsecuritycontextrepository]]
|
||||
=== DelegatingSecurityContextRepository
|
||||
@@ -122,8 +119,10 @@ The {security-api-url}org/springframework/security/web/context/DelegatingSecurit
|
||||
The most useful arrangement for this is configured with the following example, which allows the use of both xref:requestattributesecuritycontextrepository[`RequestAttributeSecurityContextRepository`] and xref:httpsecuritycontextrepository[`HttpSessionSecurityContextRepository`] simultaneously.
|
||||
|
||||
.Configure DelegatingSecurityContextRepository
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -140,7 +139,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
@@ -158,7 +158,8 @@ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http security-context-repository-ref="contextRepository">
|
||||
@@ -174,7 +175,7 @@ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -188,12 +189,14 @@ The {security-api-url}org/springframework/security/web/context/SecurityContextPe
|
||||
|
||||
image::{figures}/securitycontextpersistencefilter.png[]
|
||||
|
||||
<1> Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
<2> Next, the application is ran.
|
||||
<3> Finally, if the `SecurityContext` has changed, we save the `SecurityContext` using the `SecurityContextPersistenceRepository`.
|
||||
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
|
||||
image:{icondir}/number_2.png[] Next, the application is ran.
|
||||
|
||||
image:{icondir}/number_3.png[] Finally, if the `SecurityContext` has changed, we save the `SecurityContext` using the `SecurityContextPersistenceRepository`.
|
||||
This means that when using `SecurityContextPersistenceFilter`, just setting the `SecurityContextHolder` will ensure that the `SecurityContext` is persisted using `SecurityContextRepository`.
|
||||
|
||||
In some cases a response is committed and written to the client before the `SecurityContextPersisteneFilter` method completes.
|
||||
In some cases a response is committed and written to the client before the `SecurityContextPersistenceFilter` method completes.
|
||||
For example, if a redirect is sent to the client the response is immediately written back to the client.
|
||||
This means that establishing an `HttpSession` would not be possible in step 3 because the session id could not be included in the already written response.
|
||||
Another situation that can happen is that if a client authenticates successfully, the response is committed before `SecurityContextPersistenceFilter` completes, and the client makes a second request before the `SecurityContextPersistenceFilter` completes the wrong authentication could be present in the second request.
|
||||
@@ -207,11 +210,12 @@ The {security-api-url}org/springframework/security/web/context/SecurityContextHo
|
||||
|
||||
image::{figures}/securitycontextholderfilter.png[]
|
||||
|
||||
<1> Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
<2> Next, the application is ran.
|
||||
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
|
||||
image:{icondir}/number_2.png[] Next, the application is ran.
|
||||
|
||||
Unlike, xref:servlet/authentication/persistence.adoc#securitycontextpersistencefilter[`SecurityContextPersistenceFilter`], `SecurityContextHolderFilter` only loads the `SecurityContext` it does not save the `SecurityContext`.
|
||||
This means that when using `SecurityContextHolderFilter`, it is required that the `SecurityContext` is explicitly saved.
|
||||
|
||||
|
||||
include::partial$servlet/architecture/security-context-explicit.adoc[]
|
||||
include::partial$servlet/architecture/security-context-explicit.adoc[]
|
||||
|
||||
@@ -30,8 +30,10 @@ This class will check the current contents of the security context and, if empty
|
||||
Subclasses override the following methods to obtain this information:
|
||||
|
||||
.Override AbstractPreAuthenticatedProcessingFilter
|
||||
====
|
||||
.Java
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);
|
||||
@@ -39,14 +41,15 @@ protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest reques
|
||||
protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
protected abstract fun getPreAuthenticatedPrincipal(request: HttpServletRequest): Any?
|
||||
|
||||
protected abstract fun getPreAuthenticatedCredentials(request: HttpServletRequest): Any?
|
||||
----
|
||||
====
|
||||
======
|
||||
|
||||
|
||||
After calling these, the filter will create a `PreAuthenticatedAuthenticationToken` containing the returned data and submit it for authentication.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user