Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e38bf6e554 | |||
| 624e0da669 | |||
| b28aa6c647 | |||
| 9e83b4be25 | |||
| e9854c91ef | |||
| 0345e29afb | |||
| 8bd5795f8e | |||
| 4fbc98dcd2 | |||
| 44cdbd6cf3 | |||
| a9af8c4128 | |||
| 28c81af788 | |||
| 3a956daf0c | |||
| 9c245865d7 | |||
| 86ed937a47 | |||
| 32ec8c3ae4 | |||
| c7ffd2513a | |||
| b3e0f167ff | |||
| 8c063f8ccb | |||
| f1ca42e501 | |||
| 719149f01d | |||
| ccd30607a9 | |||
| aa1ef46d84 | |||
| 16a21264d0 | |||
| 1c0f092176 | |||
| 32d79f3f4e | |||
| 088a24cf59 | |||
| 6b6f473a1b | |||
| 29a4b2bc9b |
@@ -63,6 +63,7 @@ updateDependenciesSettings {
|
||||
})
|
||||
dependencyExcludes {
|
||||
majorVersionBump()
|
||||
minorVersionBump()
|
||||
alphaBetaVersions()
|
||||
releaseCandidatesVersions()
|
||||
milestoneVersions()
|
||||
|
||||
@@ -9,7 +9,6 @@ plugins {
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/plugins-release/' }
|
||||
@@ -76,7 +75,7 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.4.12'
|
||||
implementation 'io.projectreactor:reactor-core:3.4.13'
|
||||
implementation 'gradle.plugin.org.gretty:gretty:3.0.1'
|
||||
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
@@ -88,7 +87,7 @@ dependencies {
|
||||
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20'
|
||||
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.8.1')
|
||||
testImplementation platform('org.junit:junit-bom:5.8.2')
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
@@ -25,7 +25,7 @@ public class DocsPlugin implements Plugin<Project> {
|
||||
group = 'Distribution'
|
||||
archiveBaseName = project.rootProject.name
|
||||
archiveClassifier = 'docs'
|
||||
description = "Builds -${classifier} archive containing all " +
|
||||
description = "Builds -${archiveClassifier.get()} archive containing all " +
|
||||
"Docs for deployment at docs.spring.io"
|
||||
|
||||
from(project.tasks.api.outputs) {
|
||||
|
||||
@@ -35,11 +35,6 @@ class RepositoryConventionPlugin implements Plugin<Project> {
|
||||
mavenLocal()
|
||||
}
|
||||
mavenCentral()
|
||||
jcenter() {
|
||||
content {
|
||||
includeGroup "org.gretty"
|
||||
}
|
||||
}
|
||||
if (isSnapshot) {
|
||||
maven {
|
||||
name = 'artifactory-snapshot'
|
||||
|
||||
+12
-18
@@ -107,7 +107,7 @@ public class RepositoryConventionPluginTests {
|
||||
this.project.getPluginManager().apply(RepositoryConventionPlugin.class);
|
||||
|
||||
RepositoryHandler repositories = this.project.getRepositories();
|
||||
assertThat(repositories).hasSize(5);
|
||||
assertThat(repositories).hasSize(4);
|
||||
assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal");
|
||||
}
|
||||
|
||||
@@ -119,39 +119,33 @@ public class RepositoryConventionPluginTests {
|
||||
this.project.getPluginManager().apply(RepositoryConventionPlugin.class);
|
||||
|
||||
RepositoryHandler repositories = this.project.getRepositories();
|
||||
assertThat(repositories).hasSize(6);
|
||||
assertThat(repositories).hasSize(5);
|
||||
assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal");
|
||||
}
|
||||
|
||||
private void assertSnapshotRepository(RepositoryHandler repositories) {
|
||||
assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(6);
|
||||
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
|
||||
.isEqualTo("https://jcenter.bintray.com/");
|
||||
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(5);
|
||||
assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString())
|
||||
.isEqualTo("https://repo.maven.apache.org/maven2/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString())
|
||||
.isEqualTo("https://jcenter.bintray.com/");
|
||||
.isEqualTo("https://repo.spring.io/snapshot/");
|
||||
assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString())
|
||||
.isEqualTo("https://repo.spring.io/milestone/");
|
||||
}
|
||||
|
||||
private void assertReleaseRepository(RepositoryHandler repositories) {
|
||||
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://jcenter.bintray.com/");
|
||||
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.spring.io/release/");
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -133,8 +133,10 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
|
||||
private boolean isAuthorized(Authentication authentication) {
|
||||
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
|
||||
if (this.authorities.contains(grantedAuthority)) {
|
||||
return true;
|
||||
for (GrantedAuthority authority : this.authorities) {
|
||||
if (authority.getAuthority().equals(grantedAuthority.getAuthority())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
+3
-2
@@ -45,9 +45,10 @@ public class AuthorityReactiveAuthorizationManager<T> implements ReactiveAuthori
|
||||
@Override
|
||||
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
|
||||
// @formatter:off
|
||||
return authentication.filter((a) -> a.isAuthenticated())
|
||||
return authentication.filter(Authentication::isAuthenticated)
|
||||
.flatMapIterable(Authentication::getAuthorities)
|
||||
.any(this.authorities::contains)
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> authority.getAuthority().equals(grantedAuthority)))
|
||||
.map((granted) -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(granted, this.authorities)))
|
||||
.defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities));
|
||||
// @formatter:on
|
||||
|
||||
+26
@@ -16,12 +16,14 @@
|
||||
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -133,6 +135,30 @@ public class AuthorityAuthorizationManagerTests {
|
||||
assertThat(manager.check(authentication, object).isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAuthorityWhenUserHasCustomAuthorityThenGrantedDecision() {
|
||||
AuthorityAuthorizationManager<Object> manager = AuthorityAuthorizationManager.hasAuthority("ADMIN");
|
||||
GrantedAuthority customGrantedAuthority = () -> "ADMIN";
|
||||
|
||||
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
|
||||
Collections.singletonList(customGrantedAuthority));
|
||||
Object object = new Object();
|
||||
|
||||
assertThat(manager.check(authentication, object).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAuthorityWhenUserHasNotCustomAuthorityThenDeniedDecision() {
|
||||
AuthorityAuthorizationManager<Object> manager = AuthorityAuthorizationManager.hasAuthority("ADMIN");
|
||||
GrantedAuthority customGrantedAuthority = () -> "USER";
|
||||
|
||||
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
|
||||
Collections.singletonList(customGrantedAuthority));
|
||||
Object object = new Object();
|
||||
|
||||
assertThat(manager.check(authentication, object).isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAnyRoleWhenUserHasAnyRoleThenGrantedDecision() {
|
||||
AuthorityAuthorizationManager<Object> manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER");
|
||||
|
||||
+19
@@ -27,6 +27,7 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -88,6 +89,24 @@ public class AuthorityReactiveAuthorizationManagerTests {
|
||||
assertThat(granted).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkWhenHasCustomAuthorityAndAuthorizedThenReturnTrue() {
|
||||
GrantedAuthority customGrantedAuthority = () -> "ADMIN";
|
||||
this.authentication = new TestingAuthenticationToken("rob", "secret",
|
||||
Collections.singletonList(customGrantedAuthority));
|
||||
boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted();
|
||||
assertThat(granted).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkWhenHasCustomAuthorityAndAuthenticatedAndWrongAuthoritiesThenReturnFalse() {
|
||||
GrantedAuthority customGrantedAuthority = () -> "USER";
|
||||
this.authentication = new TestingAuthenticationToken("rob", "secret",
|
||||
Collections.singletonList(customGrantedAuthority));
|
||||
boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted();
|
||||
assertThat(granted).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkWhenHasRoleAndAuthorizedThenReturnTrue() {
|
||||
this.manager = AuthorityReactiveAuthorizationManager.hasRole("ADMIN");
|
||||
|
||||
+8
-8
@@ -8,15 +8,15 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.13")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.14")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.1")
|
||||
api platform("org.junit:junit-bom:5.8.1")
|
||||
api platform("org.junit:junit-bom:5.8.2")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.1.0")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.0")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.1")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.7"
|
||||
api "ch.qos.logback:logback-classic:1.2.9"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.14"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.19"
|
||||
@@ -26,7 +26,7 @@ dependencies {
|
||||
api "commons-codec:commons-codec:1.15"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "commons-logging:commons-logging:1.2"
|
||||
api "io.mockk:mockk:1.12.0"
|
||||
api "io.mockk:mockk:1.12.1"
|
||||
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
|
||||
api "javax.annotation:jsr250-api:1.0"
|
||||
api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2"
|
||||
@@ -53,9 +53,9 @@ dependencies {
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.1.Final"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.3.Final"
|
||||
api "org.hsqldb:hsqldb:2.6.1"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.2"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
api "org.mockito:mockito-core:3.12.4"
|
||||
api "org.mockito:mockito-inline:3.12.4"
|
||||
api "org.mockito:mockito-junit-jupiter:3.12.4"
|
||||
@@ -71,7 +71,7 @@ dependencies {
|
||||
api "org.slf4j:jcl-over-slf4j:1.7.32"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.32"
|
||||
api "org.slf4j:slf4j-api:1.7.32"
|
||||
api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE"
|
||||
api "org.springframework.ldap:spring-ldap-core:2.3.5.RELEASE"
|
||||
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
name: ROOT
|
||||
version: '5.6.0'
|
||||
version: '5.6.1'
|
||||
prerelease: '-SNAPSHOT'
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -49,7 +49,8 @@
|
||||
*** xref:servlet/authentication/events.adoc[Authentication Events]
|
||||
** xref:servlet/authorization/index.adoc[Authorization]
|
||||
*** xref:servlet/authorization/architecture.adoc[Authorization Architecture]
|
||||
*** xref:servlet/authorization/authorize-requests.adoc[Authorize HTTP Requests]
|
||||
*** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]
|
||||
*** xref:servlet/authorization/authorize-requests.adoc[Authorize HTTP Requests with FilterSecurityInterceptor]
|
||||
*** xref:servlet/authorization/expression-based.adoc[Expression-Based Access Control]
|
||||
*** xref:servlet/authorization/secure-objects.adoc[Secure Object Implementations]
|
||||
*** xref:servlet/authorization/method-security.adoc[Method Security]
|
||||
|
||||
@@ -463,7 +463,7 @@ You should instead migrate to using `DelegatingPasswordEncoder` to support secur
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public static NoOpPasswordEncoder passwordEncoder() {
|
||||
public static PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -162,7 +162,7 @@ class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
@@ -170,8 +170,6 @@ class SecurityConfig {
|
||||
authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository)
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
private fun authorizationRequestResolver(
|
||||
@@ -282,13 +280,11 @@ class OAuth2ClientSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Client {
|
||||
authorizationRequestRepository = authorizationRequestRepository()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -363,13 +359,11 @@ class OAuth2ClientSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Client {
|
||||
authenticationManager = authorizationCodeAuthenticationManager()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
private fun authorizationCodeAuthenticationManager(): ReactiveAuthenticationManager {
|
||||
|
||||
@@ -55,7 +55,7 @@ class OAuth2ClientSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Client {
|
||||
clientRegistrationRepository = clientRegistrationRepository()
|
||||
authorizedClientRepository = authorizedClientRepository()
|
||||
@@ -64,8 +64,6 @@ class OAuth2ClientSecurityConfig {
|
||||
authenticationManager = authenticationManager()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -60,7 +60,7 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Login {
|
||||
authenticationConverter = authenticationConverter()
|
||||
authenticationMatcher = authenticationMatcher()
|
||||
@@ -75,8 +75,6 @@ class OAuth2LoginSecurityConfig {
|
||||
securityContextRepository = securityContextRepository()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -158,7 +156,7 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
exceptionHandling {
|
||||
authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2")
|
||||
}
|
||||
@@ -166,8 +164,6 @@ class OAuth2LoginSecurityConfig {
|
||||
authorizationRequestResolver = authorizationRequestResolver()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver {
|
||||
@@ -243,13 +239,11 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Login {
|
||||
authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -369,11 +363,9 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -458,11 +450,9 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -536,11 +526,9 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -594,11 +582,9 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -730,7 +716,7 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
@@ -739,8 +725,6 @@ class OAuth2LoginSecurityConfig {
|
||||
logoutSuccessHandler = oidcLogoutSuccessHandler()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler {
|
||||
|
||||
@@ -341,14 +341,12 @@ class OAuth2LoginSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -411,14 +409,12 @@ class OAuth2LoginConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -505,14 +501,12 @@ class OAuth2LoginConfig {
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
oauth2Login { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
== Authorities
|
||||
xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`], discusses how all `Authentication` implementations store a list of `GrantedAuthority` objects.
|
||||
These represent the authorities that have been granted to the principal.
|
||||
The `GrantedAuthority` objects are inserted into the `Authentication` object by the `AuthenticationManager` and are later read by ``AccessDecisionManager``s when making authorization decisions.
|
||||
The `GrantedAuthority` objects are inserted into the `Authentication` object by the `AuthenticationManager` and are later read by either the `AuthorizationManager` when making authorization decisions.
|
||||
|
||||
`GrantedAuthority` is an interface with only one method:
|
||||
|
||||
@@ -19,25 +19,219 @@ String getAuthority();
|
||||
|
||||
----
|
||||
|
||||
This method allows
|
||||
``AccessDecisionManager``s to obtain a precise `String` representation of the `GrantedAuthority`.
|
||||
By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most ``AccessDecisionManager``s.
|
||||
This method allows ``AuthorizationManager``s to obtain a precise `String` representation of the `GrantedAuthority`.
|
||||
By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most ``AuthorizationManager``s and ``AccessDecisionManager``s.
|
||||
If a `GrantedAuthority` cannot be precisely represented as a `String`, the `GrantedAuthority` is considered "complex" and `getAuthority()` must return `null`.
|
||||
|
||||
An example of a "complex" `GrantedAuthority` would be an implementation that stores a list of operations and authority thresholds that apply to different customer account numbers.
|
||||
Representing this complex `GrantedAuthority` as a `String` would be quite difficult, and as a result the `getAuthority()` method should return `null`.
|
||||
This will indicate to any `AccessDecisionManager` that it will need to specifically support the `GrantedAuthority` implementation in order to understand its contents.
|
||||
This will indicate to any `AuthorizationManager` that it will need to specifically support the `GrantedAuthority` implementation in order to understand its contents.
|
||||
|
||||
Spring Security includes one concrete `GrantedAuthority` implementation, `SimpleGrantedAuthority`.
|
||||
This allows any user-specified `String` to be converted into a `GrantedAuthority`.
|
||||
All ``AuthenticationProvider``s included with the security architecture use `SimpleGrantedAuthority` to populate the `Authentication` object.
|
||||
|
||||
|
||||
[[authz-pre-invocation]]
|
||||
== Pre-Invocation Handling
|
||||
Spring Security provides interceptors which control access to secure objects such as method invocations or web requests.
|
||||
A pre-invocation decision on whether the invocation is allowed to proceed is made by the `AccessDecisionManager`.
|
||||
|
||||
=== The AuthorizationManager
|
||||
`AuthorizationManager` supersedes both <<authz-legacy-note,`AccessDecisionManager` and `AccessDecisionVoter`>>.
|
||||
|
||||
Applications that customize an `AccessDecisionManager` or `AccessDecisionVoter` are encouraged to <<authz-voter-adaptation,change to using `AuthorizationManager`>>.
|
||||
|
||||
``AuthorizationManager``s are called by the xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] and are responsible for making final access control decisions.
|
||||
The `AuthorizationManager` interface contains two methods:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
|
||||
|
||||
default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
|
||||
throws AccessDeniedException {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
The ``AuthorizationManager``'s `check` method is passed all the relevant information it needs in order to make an authorization decision.
|
||||
In particular, passing the secure `Object` enables those arguments contained in the actual secure object invocation to be inspected.
|
||||
For example, let's assume the secure object was a `MethodInvocation`.
|
||||
It would be easy to query the `MethodInvocation` for any `Customer` argument, and then implement some sort of security logic in the `AuthorizationManager` to ensure the principal is permitted to operate on that customer.
|
||||
Implementations are expected to return a positive `AuthorizationDecision` if access is granted, negative `AuthorizationDecision` if access is denied, and a null `AuthorizationDecision` when abstaining from making a decision.
|
||||
|
||||
`verify` calls `check` and subsequently throws an `AccessDeniedException` in the case of a negative `AuthorizationDecision`.
|
||||
|
||||
[[authz-delegate-authorization-manager]]
|
||||
=== Delegate-based AuthorizationManager Implementations
|
||||
Whilst users can implement their own `AuthorizationManager` to control all aspects of authorization, Spring Security ships with a delegating `AuthorizationManager` that can collaborate with individual ``AuthorizationManager``s.
|
||||
|
||||
`RequestMatcherDelegatingAuthorizationManager` will match the request with the most appropriate delegate `AuthorizationManager`.
|
||||
For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` and `AuthorizationManagerAfterMethodInterceptor`.
|
||||
|
||||
<<authz-authorization-manager-implementations>> illustrates the relevant classes.
|
||||
|
||||
[[authz-authorization-manager-implementations]]
|
||||
.Authorization Manager Implementations
|
||||
image::{figures}/authorizationhierarchy.png[]
|
||||
|
||||
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
|
||||
|
||||
[[authz-authority-authorization-manager]]
|
||||
==== AuthorityAuthorizationManager
|
||||
The most common `AuthorizationManager` provided with Spring Security is `AuthorityAuthorizationManager`.
|
||||
It is configured with a given set of authorities to look for on the current `Authentication`.
|
||||
It will return positive `AuthorizationDecision` should the `Authentication` contain any of the configured authorities.
|
||||
It will return a negative `AuthorizationDecision` otherwise.
|
||||
|
||||
[[authz-authenticated-authorization-manager]]
|
||||
==== AuthenticatedAuthorizationManager
|
||||
Another manager is the `AuthenticatedAuthorizationManager`.
|
||||
It can be used to differentiate between anonymous, fully-authenticated and remember-me authenticated users.
|
||||
Many sites allow certain limited access under remember-me authentication, but require a user to confirm their identity by logging in for full access.
|
||||
|
||||
[[authz-custom-authorization-manager]]
|
||||
==== Custom Authorization Managers
|
||||
Obviously, you can also implement a custom `AuthorizationManager` and you can put just about any access-control logic you want in it.
|
||||
It might be specific to your application (business-logic related) or it might implement some security administration logic.
|
||||
For example, you can create an implementation that can query Open Policy Agent or your own authorization database.
|
||||
|
||||
[TIP]
|
||||
You'll find a https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time[blog article] on the Spring web site which describes how to use the legacy `AccessDecisionVoter` to deny access in real-time to users whose accounts have been suspended.
|
||||
You can achieve the same outcome by implementing `AuthorizationManager` instead.
|
||||
|
||||
[[authz-voter-adaptation]]
|
||||
== Adapting AccessDecisionManager and AccessDecisionVoters
|
||||
|
||||
Previous to `AuthorizationManager`, Spring Security published <<authz-legacy-note,`AccessDecisionManager` and `AccessDecisionVoter`>>.
|
||||
|
||||
In some cases, like migrating an older application, it may be desirable to introduce an `AuthorizationManager` that invokes an `AccessDecisionManager` or `AccessDecisionVoter`.
|
||||
|
||||
To call an existing `AccessDecisionManager`, you can do:
|
||||
|
||||
.Adapting an AccessDecisionManager
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager {
|
||||
private final AccessDecisionManager accessDecisionManager;
|
||||
private final SecurityMetadataSource securityMetadataSource;
|
||||
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
|
||||
try {
|
||||
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
|
||||
this.accessDecisionManager.decide(authentication.get(), object, attributes);
|
||||
return new AuthorizationDecision(true);
|
||||
} catch (AccessDeniedException ex) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(Supplier<Authentication> authentication, Object object) {
|
||||
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
|
||||
this.accessDecisionManager.decide(authentication.get(), object, attributes);
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
And then wire it into your `SecurityFilterChain`.
|
||||
|
||||
Or to only call an `AccessDecisionVoter`, you can do:
|
||||
|
||||
.Adapting an AccessDecisionVoter
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager {
|
||||
private final AccessDecisionVoter accessDecisionVoter;
|
||||
private final SecurityMetadataSource securityMetadataSource;
|
||||
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
|
||||
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
|
||||
int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
|
||||
switch (decision) {
|
||||
case ACCESS_GRANTED:
|
||||
return new AuthorizationDecision(true);
|
||||
case ACCESS_DENIED:
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
And then wire it into your `SecurityFilterChain`.
|
||||
|
||||
[[authz-hierarchical-roles]]
|
||||
== Hierarchical Roles
|
||||
It is a common requirement that a particular role in an application should automatically "include" other roles.
|
||||
For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can.
|
||||
To achieve this, you can either make sure that all admin users are also assigned the "user" role.
|
||||
Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role.
|
||||
This can get quite complicated if you have a lot of different roles in your application.
|
||||
|
||||
The use of a role-hierarchy allows you to configure which roles (or authorities) should include others.
|
||||
An extended version of Spring Security's `RoleVoter`, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned.
|
||||
A typical configuration might look like this:
|
||||
|
||||
.Hierarchical Roles Configuration
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
AccessDecisionVoter hierarchyVoter() {
|
||||
RoleHierarchy hierarchy = new RoleHierarchyImpl();
|
||||
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
|
||||
"ROLE_STAFF > ROLE_USER\n" +
|
||||
"ROLE_USER > ROLE_GUEST");
|
||||
return new RoleHierarcyVoter(hierarchy);
|
||||
}
|
||||
----
|
||||
|
||||
.Xml
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
|
||||
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
|
||||
<constructor-arg ref="roleHierarchy" />
|
||||
</bean>
|
||||
<bean id="roleHierarchy"
|
||||
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
|
||||
<property name="hierarchy">
|
||||
<value>
|
||||
ROLE_ADMIN > ROLE_STAFF
|
||||
ROLE_STAFF > ROLE_USER
|
||||
ROLE_USER > ROLE_GUEST
|
||||
</value>
|
||||
</property>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
|
||||
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AuthorizationManager` adapted to call the above `RoleHierarchyVoter`.
|
||||
The `>` symbol can be thought of as meaning "includes".
|
||||
|
||||
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user.
|
||||
For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information.
|
||||
|
||||
[[authz-legacy-note]]
|
||||
== Legacy Authorization Components
|
||||
|
||||
[NOTE]
|
||||
Spring Security contains some legacy components.
|
||||
Since they are not yet removed, documentation is included for historical purposes.
|
||||
Their recommended replacements are above.
|
||||
|
||||
[[authz-access-decision-manager]]
|
||||
=== The AccessDecisionManager
|
||||
@@ -72,8 +266,6 @@ Whilst users can implement their own `AccessDecisionManager` to control all aspe
|
||||
.Voting Decision Manager
|
||||
image::{figures}/access-decision-voting.png[]
|
||||
|
||||
|
||||
|
||||
Using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
|
||||
The `AccessDecisionManager` then decides whether or not to throw an `AccessDeniedException` based on its assessment of the votes.
|
||||
|
||||
@@ -104,7 +296,6 @@ Like the other implementations, there is a parameter that controls the behaviour
|
||||
It is possible to implement a custom `AccessDecisionManager` that tallies votes differently.
|
||||
For example, votes from a particular `AccessDecisionVoter` might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect.
|
||||
|
||||
|
||||
[[authz-role-voter]]
|
||||
==== RoleVoter
|
||||
The most commonly used `AccessDecisionVoter` provided with Spring Security is the simple `RoleVoter`, which treats configuration attributes as simple role names and votes to grant access if the user has been assigned that role.
|
||||
@@ -130,14 +321,6 @@ Obviously, you can also implement a custom `AccessDecisionVoter` and you can put
|
||||
It might be specific to your application (business-logic related) or it might implement some security administration logic.
|
||||
For example, you'll find a https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time[blog article] on the Spring web site which describes how to use a voter to deny access in real-time to users whose accounts have been suspended.
|
||||
|
||||
|
||||
[[authz-after-invocation-handling]]
|
||||
== After Invocation Handling
|
||||
Whilst the `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` before proceeding with the secure object invocation, some applications need a way of modifying the object actually returned by the secure object invocation.
|
||||
Whilst you could easily implement your own AOP concern to achieve this, Spring Security provides a convenient hook that has several concrete implementations that integrate with its ACL capabilities.
|
||||
|
||||
<<authz-after-invocation>> illustrates Spring Security's `AfterInvocationManager` and its concrete implementations.
|
||||
|
||||
[[authz-after-invocation]]
|
||||
.After Invocation Implementation
|
||||
image::{figures}/after-invocation.png[]
|
||||
@@ -151,41 +334,3 @@ If you're using the typical Spring Security included `AccessDecisionManager` imp
|
||||
In turn, if the `AccessDecisionManager` property "`allowIfAllAbstainDecisions`" is `false`, an `AccessDeniedException` will be thrown.
|
||||
You may avoid this potential issue by either (i) setting "`allowIfAllAbstainDecisions`" to `true` (although this is generally not recommended) or (ii) simply ensure that there is at least one configuration attribute that an `AccessDecisionVoter` will vote to grant access for.
|
||||
This latter (recommended) approach is usually achieved through a `ROLE_USER` or `ROLE_AUTHENTICATED` configuration attribute.
|
||||
|
||||
|
||||
[[authz-hierarchical-roles]]
|
||||
== Hierarchical Roles
|
||||
It is a common requirement that a particular role in an application should automatically "include" other roles.
|
||||
For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can.
|
||||
To achieve this, you can either make sure that all admin users are also assigned the "user" role.
|
||||
Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role.
|
||||
This can get quite complicated if you have a lot of different roles in your application.
|
||||
|
||||
The use of a role-hierarchy allows you to configure which roles (or authorities) should include others.
|
||||
An extended version of Spring Security's <<authz-role-voter,RoleVoter>>, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned.
|
||||
A typical configuration might look like this:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
|
||||
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
|
||||
<constructor-arg ref="roleHierarchy" />
|
||||
</bean>
|
||||
<bean id="roleHierarchy"
|
||||
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
|
||||
<property name="hierarchy">
|
||||
<value>
|
||||
ROLE_ADMIN > ROLE_STAFF
|
||||
ROLE_STAFF > ROLE_USER
|
||||
ROLE_USER > ROLE_GUEST
|
||||
</value>
|
||||
</property>
|
||||
</bean>
|
||||
----
|
||||
|
||||
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AccessDecisionManager` configured with the above `RoleHierarchyVoter`.
|
||||
The `>` symbol can be thought of as meaning "includes".
|
||||
|
||||
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user.
|
||||
For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
[[servlet-authorization-authorizationfilter]]
|
||||
= Authorize HttpServletRequests with AuthorizationFilter
|
||||
:figures: servlet/authorization
|
||||
|
||||
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet-based applications.
|
||||
|
||||
[NOTE]
|
||||
`AuthorizationFilter` supersedes xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`].
|
||||
To remain backward compatible, `FilterSecurityInterceptor` remains the default.
|
||||
This section discusses how `AuthorizationFilter` works and how to override the default configuration.
|
||||
|
||||
The {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s.
|
||||
It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters].
|
||||
|
||||
You can override the default when you declare a `SecurityFilterChain`.
|
||||
Instead of using xref:servlet/authorization/authorize-http-requests.adoc#servlet-authorize-requests-defaults[`authorizeRequests`], use `authorizeHttpRequests`, like so:
|
||||
|
||||
.Use authorizeHttpRequests
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated();
|
||||
)
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This improves on `authorizeRequests` in a number of ways:
|
||||
|
||||
1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
|
||||
This simplifies reuse and customization.
|
||||
2. Delays `Authentication` lookup.
|
||||
Instead of the authentication needing to be looked up for every request, it will only look it up in requests where an authorization decision requires authentication.
|
||||
3. Bean-based configuration support.
|
||||
|
||||
When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`].
|
||||
|
||||
.Authorize HttpServletRequest
|
||||
image::{figures}/authorizationfilter.png[]
|
||||
|
||||
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
It wraps this in an `Supplier` in order to delay lookup.
|
||||
* image:{icondir}/number_2.png[] Second, `AuthorizationFilter` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain`.
|
||||
// FIXME: link to FilterInvocation
|
||||
* image:{icondir}/number_3.png[] Next, it passes the `Supplier<Authentication>` and `FilterInvocation` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`].
|
||||
** image:{icondir}/number_4.png[] If authorization is denied, an `AccessDeniedException` is thrown.
|
||||
In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`.
|
||||
** image:{icondir}/number_5.png[] If access is granted, `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
|
||||
|
||||
We can configure Spring Security to have different rules by adding more rules in order of precedence.
|
||||
|
||||
.Authorize Requests
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.authorizeHttpRequests(authorize -> authorize // <1>
|
||||
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2>
|
||||
.mvcMatchers("/admin/**").hasRole("ADMIN") // <3>
|
||||
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4>
|
||||
.anyRequest().denyAll() // <5>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
<1> There are multiple authorization rules specified.
|
||||
Each rule is considered in the order they were declared.
|
||||
<2> We specified multiple URL patterns that any user can access.
|
||||
Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about".
|
||||
<3> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN".
|
||||
You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix.
|
||||
<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA".
|
||||
You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix.
|
||||
<5> Any URL that has not already been matched on is denied access.
|
||||
This is a good strategy if you do not want to accidentally forget to update your authorization rules.
|
||||
|
||||
You can take a bean-based approach by constructing your own xref:servlet/authorization/architecture.adoc#authz-delegate-authorization-manager[`RequestMatcherDelegatingAuthorizationManager`] like so:
|
||||
|
||||
.Configure RequestMatcherDelegatingAuthorizationManager
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
|
||||
throws AuthenticationException {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().access(access)
|
||||
)
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
|
||||
RequestMatcher permitAll =
|
||||
new AndRequestMatcher(
|
||||
new MvcRequestMatcher(introspector, "/resources/**"),
|
||||
new MvcRequestMatcher(introspector, "/signup"),
|
||||
new MvcRequestMatcher(introspector, "/about"));
|
||||
RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**");
|
||||
RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**");
|
||||
RequestMatcher any = AnyRequestMatcher.INSTANCE;
|
||||
AuthorizationManager<HttpRequestServlet> manager = RequestMatcherDelegatingAuthorizationManager.builder()
|
||||
.add(permitAll, (context) -> new AuthorizationDecision(true))
|
||||
.add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
|
||||
.add(db, AuthorityAuthorizationManager.hasRole("DBA"))
|
||||
.add(any, new AuthenticatedAuthorizationManager())
|
||||
.build();
|
||||
return (context) -> manager.check(context.getRequest());
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
You can also wire xref:servlet/authorization/architecture.adoc#authz-custom-authorization-manager[your own custom authorization managers] for any request matcher.
|
||||
|
||||
Here is an example of mapping a custom authorization manager to the `my/authorized/endpoint`:
|
||||
|
||||
.Custom Authorization Manager
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.mvcMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager());
|
||||
)
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Or you can provide it for all requests as seen below:
|
||||
|
||||
.Custom Authorization Manager for All Requests
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest.access(new CustomAuthorizationManager());
|
||||
)
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
@@ -2,6 +2,10 @@
|
||||
= Authorize HttpServletRequest with FilterSecurityInterceptor
|
||||
:figures: servlet/authorization
|
||||
|
||||
[NOTE]
|
||||
`FilterSecurityInterceptor` is in the process of being replaced by xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`].
|
||||
Consider using that instead.
|
||||
|
||||
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet based applications.
|
||||
|
||||
The {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s.
|
||||
@@ -14,7 +18,7 @@ image::{figures}/filtersecurityinterceptor.png[]
|
||||
* image:{icondir}/number_2.png[] Second, `FilterSecurityInterceptor` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain` that are passed into the `FilterSecurityInterceptor`.
|
||||
// FIXME: link to FilterInvocation
|
||||
* image:{icondir}/number_3.png[] Next, it passes the `FilterInvocation` to `SecurityMetadataSource` to get the ``ConfigAttribute``s.
|
||||
* image:{icondir}/number_4.png[] Finally, it passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the `AccessDecisionManager`.
|
||||
* image:{icondir}/number_4.png[] Finally, it passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the xref:servlet/authorization.adoc#authz-access-decision-manager`AccessDecisionManager`.
|
||||
** image:{icondir}/number_5.png[] If authorization is denied, an `AccessDeniedException` is thrown.
|
||||
In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`.
|
||||
** image:{icondir}/number_6.png[] If access is granted, `FilterSecurityInterceptor` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
|
||||
@@ -24,6 +28,7 @@ In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilt
|
||||
By default, Spring Security's authorization will require all requests to be authenticated.
|
||||
The explicit configuration looks like:
|
||||
|
||||
[[servlet-authorize-requests-defaults]]
|
||||
.Every Request Must be Authenticated
|
||||
====
|
||||
.Java
|
||||
@@ -32,7 +37,7 @@ The explicit configuration looks like:
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.authorizeRequests(authorize -> authorize
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
}
|
||||
@@ -71,7 +76,7 @@ We can configure Spring Security to have different rules by adding more rules in
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.authorizeHttpRequests(authorize -> authorize // <1>
|
||||
.authorizeRequests(authorize -> authorize // <1>
|
||||
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2>
|
||||
.mvcMatchers("/admin/**").hasRole("ADMIN") // <3>
|
||||
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4>
|
||||
|
||||
@@ -323,7 +323,7 @@ Next, we can construct a `JWTProcessor`:
|
||||
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
|
||||
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
|
||||
new DefaultJWTProcessor();
|
||||
jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
|
||||
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
|
||||
return jwtProcessor;
|
||||
}
|
||||
----
|
||||
@@ -416,9 +416,9 @@ Now that we have a tenant-aware processor and a tenant-aware validator, we can p
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
|
||||
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
|
||||
(JwtValidators.createDefault(), this.jwtValidator);
|
||||
(JwtValidators.createDefault(), jwtValidator);
|
||||
decoder.setJwtValidator(validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ But, if you do need something from the request, then you can use create a custom
|
||||
----
|
||||
@Component
|
||||
public class AuthnRequestConverter implements
|
||||
Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
|
||||
Converter<Saml2AuthenticationRequestContext, AuthnRequest> {
|
||||
|
||||
private final AuthnRequestBuilder authnRequestBuilder;
|
||||
private final IssuerBuilder issuerBuilder;
|
||||
@@ -216,12 +216,12 @@ public class AuthnRequestConverter implements
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
|
||||
class AuthnRequestConverter : Converter<Saml2AuthenticationRequestContext, AuthnRequest> {
|
||||
private val authnRequestBuilder: AuthnRequestBuilder? = null
|
||||
private val issuerBuilder: IssuerBuilder? = null
|
||||
|
||||
// ... constructor
|
||||
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
|
||||
override fun convert(context: Saml2AuthenticationRequestContext): AuthnRequest {
|
||||
val myContext: MySaml2AuthenticationRequestContext = context
|
||||
val issuer: Issuer = issuerBuilder.buildObject()
|
||||
issuer.value = myContext.getIssuer()
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
aspectjVersion=1.9.7
|
||||
springJavaformatVersion=0.0.29
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.13
|
||||
springFrameworkVersion=5.3.14
|
||||
openSamlVersion=3.4.6
|
||||
version=5.6.0
|
||||
kotlinVersion=1.5.31
|
||||
samplesBranch=main
|
||||
version=5.6.1
|
||||
kotlinVersion=1.5.32
|
||||
samplesBranch=5.6.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
+13
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -204,10 +204,12 @@ public final class OAuth2AuthorizedClientProviderBuilder {
|
||||
|
||||
/**
|
||||
* Sets the maximum acceptable clock skew, which is used when checking the access
|
||||
* token expiry. An access token is considered expired if it's before
|
||||
* {@code Instant.now(this.clock) - clockSkew}.
|
||||
* token expiry. An access token is considered expired if
|
||||
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
|
||||
* {@code clock#instant()}.
|
||||
* @param clockSkew the maximum acceptable clock skew
|
||||
* @return the {@link PasswordGrantBuilder}
|
||||
* @see PasswordOAuth2AuthorizedClientProvider#setClockSkew(Duration)
|
||||
*/
|
||||
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
@@ -275,10 +277,12 @@ public final class OAuth2AuthorizedClientProviderBuilder {
|
||||
|
||||
/**
|
||||
* Sets the maximum acceptable clock skew, which is used when checking the access
|
||||
* token expiry. An access token is considered expired if it's before
|
||||
* {@code Instant.now(this.clock) - clockSkew}.
|
||||
* token expiry. An access token is considered expired if
|
||||
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
|
||||
* {@code clock#instant()}.
|
||||
* @param clockSkew the maximum acceptable clock skew
|
||||
* @return the {@link ClientCredentialsGrantBuilder}
|
||||
* @see ClientCredentialsOAuth2AuthorizedClientProvider#setClockSkew(Duration)
|
||||
*/
|
||||
public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
@@ -365,10 +369,12 @@ public final class OAuth2AuthorizedClientProviderBuilder {
|
||||
|
||||
/**
|
||||
* Sets the maximum acceptable clock skew, which is used when checking the access
|
||||
* token expiry. An access token is considered expired if it's before
|
||||
* {@code Instant.now(this.clock) - clockSkew}.
|
||||
* token expiry. An access token is considered expired if
|
||||
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
|
||||
* {@code clock#instant()}.
|
||||
* @param clockSkew the maximum acceptable clock skew
|
||||
* @return the {@link RefreshTokenGrantBuilder}
|
||||
* @see RefreshTokenOAuth2AuthorizedClientProvider#setClockSkew(Duration)
|
||||
*/
|
||||
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
|
||||
+13
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -225,10 +225,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
|
||||
|
||||
/**
|
||||
* Sets the maximum acceptable clock skew, which is used when checking the access
|
||||
* token expiry. An access token is considered expired if it's before
|
||||
* {@code Instant.now(this.clock) - clockSkew}.
|
||||
* token expiry. An access token is considered expired if
|
||||
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
|
||||
* {@code clock#instant()}.
|
||||
* @param clockSkew the maximum acceptable clock skew
|
||||
* @return the {@link ClientCredentialsGrantBuilder}
|
||||
* @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration)
|
||||
*/
|
||||
public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
@@ -297,10 +299,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
|
||||
|
||||
/**
|
||||
* Sets the maximum acceptable clock skew, which is used when checking the access
|
||||
* token expiry. An access token is considered expired if it's before
|
||||
* {@code Instant.now(this.clock) - clockSkew}.
|
||||
* token expiry. An access token is considered expired if
|
||||
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
|
||||
* {@code clock#instant()}.
|
||||
* @param clockSkew the maximum acceptable clock skew
|
||||
* @return the {@link PasswordGrantBuilder}
|
||||
* @see PasswordReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration)
|
||||
*/
|
||||
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
@@ -368,10 +372,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
|
||||
|
||||
/**
|
||||
* Sets the maximum acceptable clock skew, which is used when checking the access
|
||||
* token expiry. An access token is considered expired if it's before
|
||||
* {@code Instant.now(this.clock) - clockSkew}.
|
||||
* token expiry. An access token is considered expired if
|
||||
* {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time
|
||||
* {@code clock#instant()}.
|
||||
* @param clockSkew the maximum acceptable clock skew
|
||||
* @return the {@link RefreshTokenGrantBuilder}
|
||||
* @see RefreshTokenReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration)
|
||||
*/
|
||||
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -54,7 +54,7 @@ public final class JwtClaimValidator<T> implements OAuth2TokenValidator<Jwt> {
|
||||
Assert.notNull(test, "test can not be null");
|
||||
this.claim = claim;
|
||||
this.test = test;
|
||||
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "The " + this.claim + " claim is not valid",
|
||||
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The " + this.claim + " claim is not valid",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 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,10 +16,14 @@
|
||||
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -45,7 +49,9 @@ public class JwtClaimValidatorTests {
|
||||
@Test
|
||||
public void validateWhenClaimFailsTheTestThenReturnsFailure() {
|
||||
Jwt jwt = TestJwts.jwt().claim(JwtClaimNames.ISS, "http://abc").build();
|
||||
Collection<OAuth2Error> details = this.validator.validate(jwt).getErrors();
|
||||
assertThat(this.validator.validate(jwt).getErrors().isEmpty()).isFalse();
|
||||
assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,7 @@ import java.time.ZoneId;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -64,6 +65,7 @@ public class JwtTimestampValidatorTests {
|
||||
.collect(Collectors.toList());
|
||||
// @formatter:on
|
||||
assertThat(messages).contains("Jwt expired at " + oneHourAgo);
|
||||
assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,6 +80,7 @@ public class JwtTimestampValidatorTests {
|
||||
.collect(Collectors.toList());
|
||||
// @formatter:on
|
||||
assertThat(messages).contains("Jwt used before " + oneHourFromNow);
|
||||
assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -10,8 +10,6 @@ plugins {
|
||||
id "io.spring.ge.conventions" version "0.0.7"
|
||||
}
|
||||
|
||||
enableFeaturePreview("VERSION_ORDERING_V2")
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
+11
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 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,7 +17,6 @@
|
||||
package org.springframework.security.web.server.header;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -41,8 +40,16 @@ public class StaticServerHttpHeadersWriter implements ServerHttpHeadersWriter {
|
||||
@Override
|
||||
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
boolean containsOneHeaderToAdd = Collections.disjoint(headers.keySet(), this.headersToAdd.keySet());
|
||||
if (containsOneHeaderToAdd) {
|
||||
// Note: We need to ensure that the following algorithm compares headers
|
||||
// case insensitively, which should be true of headers.containsKey().
|
||||
boolean containsNoHeadersToAdd = true;
|
||||
for (String headerName : this.headersToAdd.keySet()) {
|
||||
if (headers.containsKey(headerName)) {
|
||||
containsNoHeadersToAdd = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (containsNoHeadersToAdd) {
|
||||
this.headersToAdd.forEach(headers::put);
|
||||
}
|
||||
return Mono.empty();
|
||||
|
||||
+22
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2021 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,11 +16,14 @@
|
||||
|
||||
package org.springframework.security.web.server.header;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -56,6 +59,24 @@ public class StaticServerHttpHeadersWriterTests {
|
||||
.containsOnly(headerValue);
|
||||
}
|
||||
|
||||
// gh-10557
|
||||
@Test
|
||||
public void writeHeadersWhenHeaderWrittenWithDifferentCaseThenDoesNotWriteHeaders() {
|
||||
String headerName = HttpHeaders.CACHE_CONTROL.toLowerCase(Locale.ROOT);
|
||||
String headerValue = "max-age=120";
|
||||
this.headers.set(headerName, headerValue);
|
||||
// Note: This test inverts which collection uses case sensitive headers,
|
||||
// due to the fact that gh-10557 reports NettyHeadersAdapter as the
|
||||
// response headers implementation, which is not accessible here.
|
||||
HttpHeaders caseSensitiveHeaders = new HttpHeaders(new LinkedMultiValueMap<>());
|
||||
caseSensitiveHeaders.set(HttpHeaders.CACHE_CONTROL, CacheControlServerHttpHeadersWriter.CACHE_CONTRTOL_VALUE);
|
||||
caseSensitiveHeaders.set(HttpHeaders.PRAGMA, CacheControlServerHttpHeadersWriter.PRAGMA_VALUE);
|
||||
caseSensitiveHeaders.set(HttpHeaders.EXPIRES, CacheControlServerHttpHeadersWriter.EXPIRES_VALUE);
|
||||
this.writer = new StaticServerHttpHeadersWriter(caseSensitiveHeaders);
|
||||
this.writer.writeHttpHeaders(this.exchange);
|
||||
assertThat(this.headers.get(headerName)).containsOnly(headerValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeHeadersWhenMultiHeaderThenWritesAllHeaders() {
|
||||
this.writer = StaticServerHttpHeadersWriter.builder()
|
||||
|
||||
Reference in New Issue
Block a user