Forráskód Böngészése

Add AuthorizationManagerFactory

Signed-off-by: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com>
Steve Riesenberg 2 hete
szülő
commit
eeb4574bb3
37 módosított fájl, 2718 hozzáadás és 177 törlés
  1. 6 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java
  2. 57 35
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java
  3. 322 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java
  4. 53 1
      core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java
  5. 113 64
      core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java
  6. 29 6
      core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java
  7. 7 7
      core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java
  8. 124 0
      core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java
  9. 123 0
      core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java
  10. 2 1
      core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRootTests.java
  11. 100 0
      core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java
  12. 53 11
      data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java
  13. 16 15
      data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java
  14. 37 0
      docs/modules/ROOT/pages/servlet/authorization/architecture.adoc
  15. 19 0
      docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc
  16. 18 0
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
  17. 1 0
      docs/modules/ROOT/pages/whats-new.adoc
  18. 77 0
      docs/src/test/java/org/springframework/security/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfiguration.java
  19. 208 0
      docs/src/test/java/org/springframework/security/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfigurationTests.java
  20. 48 0
      docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactory.java
  21. 137 0
      docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactoryTests.java
  22. 57 0
      docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactory.java
  23. 191 0
      docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactoryTests.java
  24. 62 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfiguration.kt
  25. 229 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfigurationTests.kt
  26. 44 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactory.kt
  27. 131 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactoryTests.kt
  28. 50 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactory.kt
  29. 215 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactoryTests.kt
  30. 40 0
      docs/src/test/resources/org/springframework/security/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfiguration.xml
  31. 1 0
      etc/checkstyle/checkstyle-suppressions.xml
  32. 11 11
      messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java
  33. 9 5
      messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java
  34. 18 8
      web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java
  35. 22 10
      web/src/main/java/org/springframework/security/web/access/expression/DefaultWebSecurityExpressionHandler.java
  36. 62 0
      web/src/main/java/org/springframework/security/web/access/expression/FilterInvocationExpressionRoot.java
  37. 26 3
      web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java

+ 6 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java

@@ -39,6 +39,7 @@ import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar;
 import org.springframework.security.aot.hint.SecurityHintsRegistrar;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
 import org.springframework.security.authorization.method.MethodInvocationResult;
@@ -121,6 +122,11 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, Applicati
 		this.expressionHandler.setRoleHierarchy(roleHierarchy);
 	}
 
+	@Autowired(required = false)
+	void setAuthorizationManagerFactory(AuthorizationManagerFactory<MethodInvocation> authorizationManagerFactory) {
+		this.expressionHandler.setAuthorizationManagerFactory(authorizationManagerFactory);
+	}
+
 	@Autowired(required = false)
 	void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
 		this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults);

+ 57 - 35
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java

@@ -18,7 +18,6 @@ package org.springframework.security.config.annotation.web.configurers;
 
 import java.util.List;
 import java.util.function.Function;
-import java.util.function.Supplier;
 
 import jakarta.servlet.http.HttpServletRequest;
 
@@ -27,13 +26,12 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.core.ResolvableType;
 import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
-import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
-import org.springframework.security.authorization.AuthorityAuthorizationManager;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.authorization.AuthorizationManagers;
-import org.springframework.security.authorization.SingleResultAuthorizationManager;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
 import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
 import org.springframework.security.config.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
@@ -46,13 +44,13 @@ import org.springframework.security.web.access.intercept.RequestMatcherDelegatin
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcherEntry;
 import org.springframework.util.Assert;
-import org.springframework.util.function.SingletonSupplier;
 
 /**
  * Adds a URL based authorization using {@link AuthorizationManager}.
  *
  * @param <H> the type of {@link HttpSecurityBuilder} that is being configured.
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 5.5
  */
 public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder<H>>
@@ -62,9 +60,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 
 	private final AuthorizationEventPublisher publisher;
 
-	private final Supplier<RoleHierarchy> roleHierarchy;
-
-	private String rolePrefix = "ROLE_";
+	private final AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory;
 
 	private ObjectPostProcessor<AuthorizationManager<HttpServletRequest>> postProcessor = ObjectPostProcessor
 		.identity();
@@ -81,13 +77,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		else {
 			this.publisher = new SpringAuthorizationEventPublisher(context);
 		}
-		this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0)
-				? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy());
-		String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
-		if (grantedAuthorityDefaultsBeanNames.length > 0) {
-			GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class);
-			this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
-		}
+		this.authorizationManagerFactory = getAuthorizationManagerFactory(context);
 		ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class,
 				ResolvableType.forClassWithGenerics(AuthorizationManager.class, HttpServletRequest.class));
 		ObjectProvider<ObjectPostProcessor<AuthorizationManager<HttpServletRequest>>> provider = context
@@ -95,6 +85,35 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		provider.ifUnique((postProcessor) -> this.postProcessor = postProcessor);
 	}
 
+	private AuthorizationManagerFactory<RequestAuthorizationContext> getAuthorizationManagerFactory(
+			ApplicationContext context) {
+		ResolvableType authorizationManagerFactoryType = ResolvableType
+			.forClassWithGenerics(AuthorizationManagerFactory.class, RequestAuthorizationContext.class);
+
+		// Handle fallback to generic type
+		if (context.getBeanNamesForType(authorizationManagerFactoryType).length == 0) {
+			authorizationManagerFactoryType = ResolvableType.forClassWithGenerics(AuthorizationManagerFactory.class,
+					Object.class);
+		}
+
+		ObjectProvider<AuthorizationManagerFactory<RequestAuthorizationContext>> authorizationManagerFactoryProvider = context
+			.getBeanProvider(authorizationManagerFactoryType);
+
+		return authorizationManagerFactoryProvider.getIfAvailable(() -> {
+			RoleHierarchy roleHierarchy = context.getBeanProvider(RoleHierarchy.class)
+				.getIfAvailable(NullRoleHierarchy::new);
+			GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBeanProvider(GrantedAuthorityDefaults.class)
+				.getIfAvailable();
+			String rolePrefix = (grantedAuthorityDefaults != null) ? grantedAuthorityDefaults.getRolePrefix() : "ROLE_";
+
+			DefaultAuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>();
+			authorizationManagerFactory.setRoleHierarchy(roleHierarchy);
+			authorizationManagerFactory.setRolePrefix(rolePrefix);
+
+			return authorizationManagerFactory;
+		});
+	}
+
 	/**
 	 * The {@link AuthorizationManagerRequestMatcherRegistry} is what users will interact
 	 * with after applying the {@link AuthorizeHttpRequestsConfigurer}.
@@ -173,7 +192,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		@Override
 		protected AuthorizedUrl chainRequestMatchers(List<RequestMatcher> requestMatchers) {
 			this.unmappedMatchers = requestMatchers;
-			return new AuthorizedUrl(requestMatchers);
+			return new AuthorizedUrl(requestMatchers, AuthorizeHttpRequestsConfigurer.this.authorizationManagerFactory);
 		}
 
 		/**
@@ -201,20 +220,31 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 
 		private final List<? extends RequestMatcher> matchers;
 
+		private AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory;
+
 		private boolean not;
 
 		/**
 		 * Creates an instance.
 		 * @param matchers the {@link RequestMatcher} instances to map
+		 * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} for
+		 * creating instances of {@link AuthorizationManager}
 		 */
-		AuthorizedUrl(List<? extends RequestMatcher> matchers) {
+		AuthorizedUrl(List<? extends RequestMatcher> matchers,
+				AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory) {
 			this.matchers = matchers;
+			this.authorizationManagerFactory = authorizationManagerFactory;
 		}
 
 		protected List<? extends RequestMatcher> getMatchers() {
 			return this.matchers;
 		}
 
+		void setAuthorizationManagerFactory(
+				AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory) {
+			this.authorizationManagerFactory = authorizationManagerFactory;
+		}
+
 		/**
 		 * Negates the following authorization rule.
 		 * @return the {@link AuthorizedUrl} for further customization
@@ -231,7 +261,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry permitAll() {
-			return access(SingleResultAuthorizationManager.permitAll());
+			return access(this.authorizationManagerFactory.permitAll());
 		}
 
 		/**
@@ -240,7 +270,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry denyAll() {
-			return access(SingleResultAuthorizationManager.denyAll());
+			return access(this.authorizationManagerFactory.denyAll());
 		}
 
 		/**
@@ -251,8 +281,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
-			return access(withRoleHierarchy(AuthorityAuthorizationManager
-				.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })));
+			return access(this.authorizationManagerFactory.hasRole(role));
 		}
 
 		/**
@@ -264,8 +293,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) {
-			return access(withRoleHierarchy(
-					AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles)));
+			return access(this.authorizationManagerFactory.hasAnyRole(roles));
 		}
 
 		/**
@@ -275,7 +303,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) {
-			return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)));
+			return access(this.authorizationManagerFactory.hasAuthority(authority));
 		}
 
 		/**
@@ -286,13 +314,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) {
-			return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
-		}
-
-		private AuthorityAuthorizationManager<RequestAuthorizationContext> withRoleHierarchy(
-				AuthorityAuthorizationManager<RequestAuthorizationContext> manager) {
-			manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get());
-			return manager;
+			return access(this.authorizationManagerFactory.hasAnyAuthority(authorities));
 		}
 
 		/**
@@ -301,7 +323,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customizations
 		 */
 		public AuthorizationManagerRequestMatcherRegistry authenticated() {
-			return access(AuthenticatedAuthorizationManager.authenticated());
+			return access(this.authorizationManagerFactory.authenticated());
 		}
 
 		/**
@@ -313,7 +335,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @see RememberMeConfigurer
 		 */
 		public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() {
-			return access(AuthenticatedAuthorizationManager.fullyAuthenticated());
+			return access(this.authorizationManagerFactory.fullyAuthenticated());
 		}
 
 		/**
@@ -324,7 +346,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @see RememberMeConfigurer
 		 */
 		public AuthorizationManagerRequestMatcherRegistry rememberMe() {
-			return access(AuthenticatedAuthorizationManager.rememberMe());
+			return access(this.authorizationManagerFactory.rememberMe());
 		}
 
 		/**
@@ -334,7 +356,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @since 5.8
 		 */
 		public AuthorizationManagerRequestMatcherRegistry anonymous() {
-			return access(AuthenticatedAuthorizationManager.anonymous());
+			return access(this.authorizationManagerFactory.anonymous());
 		}
 
 		/**

+ 322 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.Set;
 import java.util.function.Supplier;
 
 import io.micrometer.observation.Observation;
@@ -36,14 +37,19 @@ import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.event.EventListener;
+import org.springframework.http.HttpMethod;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
 import org.springframework.security.authentication.RememberMeAuthenticationToken;
 import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
+import org.springframework.security.authorization.AuthorityAuthorizationManager;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.authorization.AuthorizationObservationContext;
+import org.springframework.security.authorization.SingleResultAuthorizationManager;
 import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
 import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
 import org.springframework.security.config.ObjectPostProcessor;
@@ -82,13 +88,17 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.springframework.security.config.Customizer.withDefaults;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -170,6 +180,26 @@ public class AuthorizeHttpRequestsConfigurerTests {
 			.withMessageContaining("manager cannot be null");
 	}
 
+	@Test
+	public void configureWhenCustomAuthorizationManagerFactoryRegisteredThenUsed() {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+		this.spring.register(AuthorizationManagerFactoryConfig.class).autowire();
+		verify(authorizationManagerFactory).permitAll();
+		verify(authorizationManagerFactory).denyAll();
+		verify(authorizationManagerFactory).hasRole("ADMIN");
+		verify(authorizationManagerFactory).hasAnyRole("USER", "ADMIN");
+		verify(authorizationManagerFactory).hasAuthority("write");
+		verify(authorizationManagerFactory).hasAnyAuthority("resource.read", "read");
+		verify(authorizationManagerFactory).authenticated();
+		verify(authorizationManagerFactory).fullyAuthenticated();
+		verify(authorizationManagerFactory).rememberMe();
+		verify(authorizationManagerFactory).anonymous();
+		verifyNoMoreInteractions(authorizationManagerFactory);
+	}
+
 	@Test
 	public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() {
 		this.spring.register(ObjectPostProcessorConfig.class).autowire();
@@ -538,6 +568,205 @@ public class AuthorizeHttpRequestsConfigurerTests {
 		this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
 	}
 
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndPermitAllThenRespondsWithOk() throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> permitAll = spy(SingleResultAuthorizationManager.permitAll());
+		given(authorizationManagerFactory.permitAll()).willReturn(permitAll);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/public").with(anonymous());
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(permitAll).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(permitAll);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndDenyAllThenRespondsWithForbidden()
+			throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> denyAll = spy(SingleResultAuthorizationManager.denyAll());
+		given(authorizationManagerFactory.denyAll()).willReturn(denyAll);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/private").with(user("user"));
+		this.mvc.perform(request).andExpect(status().isForbidden());
+		verify(denyAll).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(denyAll);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndHasRoleThenRespondsWithOk() throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> hasRole = spy(AuthorityAuthorizationManager.hasRole("ADMIN"));
+		given(authorizationManagerFactory.hasRole(anyString())).willReturn(hasRole);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/admin").with(user("admin").roles("ADMIN"));
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(hasRole).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(hasRole);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndHasAnyRoleThenRespondsWithOk() throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> hasAnyRole = spy(
+				AuthorityAuthorizationManager.hasAnyRole("USER", "ADMIN"));
+		given(authorizationManagerFactory.hasAnyRole(any(String[].class))).willReturn(hasAnyRole);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/user").with(user("user").roles("USER"));
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(hasAnyRole).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(hasAnyRole);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void postWhenCustomAuthorizationManagerFactoryRegisteredAndHasAuthorityThenRespondsWithOk()
+			throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> hasAuthority = spy(
+				AuthorityAuthorizationManager.hasAuthority("write"));
+		given(authorizationManagerFactory.hasAuthority(anyString())).willReturn(hasAuthority);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = post("/resource")
+			.with(user("user").authorities(new SimpleGrantedAuthority("write")))
+			.with(csrf());
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(hasAuthority).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(hasAuthority);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndHasAnyAuthorityThenRespondsWithOk()
+			throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> hasAnyAuthority = spy(
+				AuthorityAuthorizationManager.hasAnyAuthority("resource.read", "read"));
+		given(authorizationManagerFactory.hasAnyAuthority(any(String[].class))).willReturn(hasAnyAuthority);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/resource")
+			.with(user("user").authorities(new SimpleGrantedAuthority("read")));
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(hasAnyAuthority).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(hasAnyAuthority);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndAuthenticatedThenRespondsWithOk()
+			throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> authenticated = spy(
+				AuthenticatedAuthorizationManager.authenticated());
+		given(authorizationManagerFactory.authenticated()).willReturn(authenticated);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/authenticated").with(user("user"));
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(authenticated).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(authenticated);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndFullyAuthenticatedThenRespondsWithOk()
+			throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> fullyAuthenticated = spy(
+				AuthenticatedAuthorizationManager.fullyAuthenticated());
+		given(authorizationManagerFactory.fullyAuthenticated()).willReturn(fullyAuthenticated);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/fully-authenticated").with(user("user"));
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(fullyAuthenticated).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(fullyAuthenticated);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndRememberMeThenRespondsWithOk() throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> rememberMe = spy(
+				AuthenticatedAuthorizationManager.rememberMe());
+		given(authorizationManagerFactory.rememberMe()).willReturn(rememberMe);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/remember-me")
+			.with(authentication(new RememberMeAuthenticationToken("test", "user", Set.of())));
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(rememberMe).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(rememberMe);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndAnonymousThenRespondsWithOk() throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+		AuthorizationManager<RequestAuthorizationContext> anonymous = spy(
+				AuthenticatedAuthorizationManager.anonymous());
+		given(authorizationManagerFactory.anonymous()).willReturn(anonymous);
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory;
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire();
+		MockHttpServletRequestBuilder request = get("/anonymous").with(anonymous());
+		this.mvc.perform(request).andExpect(status().isOk());
+		verify(anonymous).authorize(any(), any(RequestAuthorizationContext.class));
+		verifyNoMoreInteractions(anonymous);
+		verifyNoInteractions(authorizationManager);
+	}
+
+	@Test
+	public void getWhenCustomAuthorizationManagerFactoryRegisteredAndAccessThenRespondsWithForbidden()
+			throws Exception {
+		AuthorizationManager<RequestAuthorizationContext> authorizationManager = mock();
+		AuthorizationManagerFactoryConfig.authorizationManagerFactory = mockAuthorizationManagerFactory(
+				authorizationManager);
+
+		this.spring.register(AuthorizationManagerFactoryConfig.class).autowire();
+		MockHttpServletRequestBuilder request = get("/").with(user("user"));
+		this.mvc.perform(request).andExpect(status().isForbidden());
+		verifyNoInteractions(authorizationManager);
+	}
+
 	@Test
 	public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk()
 			throws Exception {
@@ -587,6 +816,23 @@ public class AuthorizeHttpRequestsConfigurerTests {
 		};
 	}
 
+	private AuthorizationManagerFactory<RequestAuthorizationContext> mockAuthorizationManagerFactory(
+			AuthorizationManager<RequestAuthorizationContext> authorizationManager) {
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory = mock();
+		given(authorizationManagerFactory.permitAll()).willReturn(authorizationManager);
+		given(authorizationManagerFactory.denyAll()).willReturn(authorizationManager);
+		given(authorizationManagerFactory.hasRole(anyString())).willReturn(authorizationManager);
+		given(authorizationManagerFactory.hasAnyRole(any(String[].class))).willReturn(authorizationManager);
+		given(authorizationManagerFactory.hasAuthority(anyString())).willReturn(authorizationManager);
+		given(authorizationManagerFactory.hasAnyAuthority(any(String[].class))).willReturn(authorizationManager);
+		given(authorizationManagerFactory.authenticated()).willReturn(authorizationManager);
+		given(authorizationManagerFactory.fullyAuthenticated()).willReturn(authorizationManager);
+		given(authorizationManagerFactory.rememberMe()).willReturn(authorizationManager);
+		given(authorizationManagerFactory.anonymous()).willReturn(authorizationManager);
+
+		return authorizationManagerFactory;
+	}
+
 	@Test
 	public void getWhenFullyAuthenticatedConfiguredAndRememberMeTokenThenRespondsWithUnauthorized() throws Exception {
 		this.spring.register(FullyAuthenticatedConfig.class, BasicController.class).autowire();
@@ -850,6 +1096,41 @@ public class AuthorizeHttpRequestsConfigurerTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class AuthorizationManagerFactoryConfig {
+
+		static AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory;
+
+		@Bean
+		AuthorizationManagerFactory<RequestAuthorizationContext> authorizationManagerFactory() {
+			return authorizationManagerFactory;
+		}
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize
+					.requestMatchers("/public").permitAll()
+					.requestMatchers("/private").denyAll()
+					.requestMatchers("/admin").hasRole("ADMIN")
+					.requestMatchers("/user").hasAnyRole("USER", "ADMIN")
+					.requestMatchers(HttpMethod.POST, "/resource").hasAuthority("write")
+					.requestMatchers("/resource").hasAnyAuthority("resource.read", "read")
+					.requestMatchers("/authenticated").authenticated()
+					.requestMatchers("/fully-authenticated").fullyAuthenticated()
+					.requestMatchers("/remember-me").rememberMe()
+					.requestMatchers("/anonymous").anonymous()
+					.anyRequest().access((authentication, context) -> new AuthorizationDecision(false))
+				);
+			// @formatter:on
+
+			return http.build();
+		}
+
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class ObjectPostProcessorConfig {
@@ -1295,6 +1576,47 @@ public class AuthorizeHttpRequestsConfigurerTests {
 
 	}
 
+	@RestController
+	static class AccessTestController {
+
+		@RequestMapping("/public")
+		void publicEndpoint() {
+		}
+
+		@RequestMapping("/private")
+		void privateEndpoint() {
+		}
+
+		@RequestMapping("/admin")
+		void adminEndpoint() {
+		}
+
+		@RequestMapping("/user")
+		void userEndpoint() {
+		}
+
+		@RequestMapping("/resource")
+		void resourceEndpoint() {
+		}
+
+		@RequestMapping("/authenticated")
+		void authenticatedEndpoint() {
+		}
+
+		@RequestMapping("/fully-authenticated")
+		void fullyAuthenticatedEndpoint() {
+		}
+
+		@RequestMapping("/remember-me")
+		void rememberMeEndpoint() {
+		}
+
+		@RequestMapping("/anonymous")
+		void anonymousEndpoint() {
+		}
+
+	}
+
 	@Configuration
 	static class ObservationRegistryConfig {
 

+ 53 - 1
core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java

@@ -28,6 +28,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
 import org.springframework.security.access.PermissionEvaluator;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.Assert;
 
@@ -38,6 +40,7 @@ import org.springframework.util.Assert;
  *
  * @author Luke Taylor
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 3.1
  */
 public abstract class AbstractSecurityExpressionHandler<T>
@@ -49,6 +52,8 @@ public abstract class AbstractSecurityExpressionHandler<T>
 
 	private @Nullable RoleHierarchy roleHierarchy;
 
+	private AuthorizationManagerFactory<T> authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>();
+
 	private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator();
 
 	@Override
@@ -106,11 +111,58 @@ public abstract class AbstractSecurityExpressionHandler<T>
 	protected abstract SecurityExpressionOperations createSecurityExpressionRoot(
 			@Nullable Authentication authentication, T invocation);
 
+	/**
+	 * Sets the {@link AuthorizationManagerFactory} to be used. The default is
+	 * {@link DefaultAuthorizationManagerFactory}.
+	 * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} to use.
+	 * Cannot be null.
+	 * @since 7.0
+	 */
+	public final void setAuthorizationManagerFactory(AuthorizationManagerFactory<T> authorizationManagerFactory) {
+		Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory cannot be null");
+		this.authorizationManagerFactory = authorizationManagerFactory;
+	}
+
+	protected final AuthorizationManagerFactory<T> getAuthorizationManagerFactory() {
+		return this.authorizationManagerFactory;
+	}
+
+	/**
+	 * Allows accessing the {@link DefaultAuthorizationManagerFactory} for getting and
+	 * setting defaults. This method will be removed in Spring Security 8.
+	 * @return the {@link DefaultAuthorizationManagerFactory}
+	 * @throws IllegalStateException if a different {@link AuthorizationManagerFactory}
+	 * was already set
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
+	 */
+	@Deprecated(since = "7.0")
+	protected final DefaultAuthorizationManagerFactory<T> getDefaultAuthorizationManagerFactory() {
+		if (!(this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory<T> defaultAuthorizationManagerFactory)) {
+			throw new IllegalStateException(
+					"authorizationManagerFactory must be an instance of DefaultAuthorizationManagerFactory");
+		}
+
+		return defaultAuthorizationManagerFactory;
+	}
+
+	/**
+	 * @deprecated Use {@link #getDefaultAuthorizationManagerFactory()} instead
+	 */
+	@Deprecated(since = "7.0")
 	protected @Nullable RoleHierarchy getRoleHierarchy() {
 		return this.roleHierarchy;
 	}
 
-	public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
+	/**
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
+	 */
+	@Deprecated(since = "7.0")
+	public void setRoleHierarchy(@Nullable RoleHierarchy roleHierarchy) {
+		if (roleHierarchy != null) {
+			getDefaultAuthorizationManagerFactory().setRoleHierarchy(roleHierarchy);
+		}
 		this.roleHierarchy = roleHierarchy;
 	}
 

+ 113 - 64
core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java

@@ -17,8 +17,6 @@
 package org.springframework.security.access.expression;
 
 import java.io.Serializable;
-import java.util.Collection;
-import java.util.Set;
 import java.util.function.Supplier;
 
 import org.jspecify.annotations.Nullable;
@@ -26,10 +24,11 @@ import org.jspecify.annotations.Nullable;
 import org.springframework.security.access.PermissionEvaluator;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
-import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
+import org.springframework.security.authorization.AuthorizationResult;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.util.Assert;
 import org.springframework.util.function.SingletonSupplier;
 
@@ -38,19 +37,18 @@ import org.springframework.util.function.SingletonSupplier;
  *
  * @author Luke Taylor
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 3.0
  */
-public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
+public abstract class SecurityExpressionRoot<T extends @Nullable Object> implements SecurityExpressionOperations {
 
 	private final Supplier<Authentication> authentication;
 
-	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
-
-	private @Nullable RoleHierarchy roleHierarchy;
+	private String defaultRolePrefix = "ROLE_";
 
-	private @Nullable Set<String> roles;
+	private final T object;
 
-	private String defaultRolePrefix = "ROLE_";
+	private AuthorizationManagerFactory<T> authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>();
 
 	/**
 	 * Allows "permitAll" expression
@@ -77,9 +75,12 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
 	/**
 	 * Creates a new instance
 	 * @param authentication the {@link Authentication} to use. Cannot be null.
+	 * @deprecated Use {@link #SecurityExpressionRoot(Supplier, Object)} instead
 	 */
+	@Deprecated(since = "7.0")
+	@SuppressWarnings("NullAway")
 	public SecurityExpressionRoot(@Nullable Authentication authentication) {
-		this(() -> authentication);
+		this(() -> authentication, null);
 	}
 
 	/**
@@ -88,44 +89,70 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
 	 * @param authentication the {@link Supplier} of the {@link Authentication} to use.
 	 * Cannot be null.
 	 * @since 5.8
+	 * @deprecated Use {@link #SecurityExpressionRoot(Supplier, Object)} instead
+	 */
+	@Deprecated(since = "7.0")
+	@SuppressWarnings("NullAway")
+	public SecurityExpressionRoot(Supplier<@Nullable Authentication> authentication) {
+		this(authentication, null);
+	}
+
+	/**
+	 * Creates a new instance that uses lazy initialization of the {@link Authentication}
+	 * object.
+	 * @param authentication the {@link Supplier} of the {@link Authentication} to use.
+	 * Cannot be null.
+	 * @param object the object being authorized
+	 * @since 7.0
 	 */
-	public SecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication) {
+	public SecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication, T object) {
 		this.authentication = SingletonSupplier.of(() -> {
 			Authentication value = authentication.get();
 			Assert.notNull(value, "Authentication object cannot be null");
 			return value;
 		});
+		this.object = object;
 	}
 
 	@Override
 	public final boolean hasAuthority(String authority) {
-		return hasAnyAuthority(authority);
+		return isGranted(this.authorizationManagerFactory.hasAnyAuthority(authority));
 	}
 
 	@Override
 	public final boolean hasAnyAuthority(String... authorities) {
-		return hasAnyAuthorityName(null, authorities);
+		return isGranted(this.authorizationManagerFactory.hasAnyAuthority(authorities));
 	}
 
 	@Override
 	public final boolean hasRole(String role) {
-		return hasAnyRole(role);
+		if (this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory<T>) {
+			// To provide passivity for old behavior where hasRole('ROLE_A') is allowed,
+			// we strip the role prefix when found.
+			// TODO: Remove in favor of fixing inconsistent behavior?
+			String rolePrefix = this.defaultRolePrefix;
+			if (role.startsWith(rolePrefix)) {
+				role = role.substring(rolePrefix.length());
+			}
+		}
+		return isGranted(this.authorizationManagerFactory.hasRole(role));
 	}
 
 	@Override
 	public final boolean hasAnyRole(String... roles) {
-		return hasAnyAuthorityName(this.defaultRolePrefix, roles);
-	}
-
-	private boolean hasAnyAuthorityName(@Nullable String prefix, String... roles) {
-		Set<String> roleSet = getAuthoritySet();
-		for (String role : roles) {
-			String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
-			if (roleSet.contains(defaultedRole)) {
-				return true;
+		if (this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory<T>) {
+			// To provide passivity for old behavior where hasRole('ROLE_A') is allowed,
+			// we strip the role prefix when found.
+			// TODO: Remove in favor of fixing inconsistent behavior?
+			String rolePrefix = this.defaultRolePrefix;
+			for (int index = 0; index < roles.length; index++) {
+				String role = roles[index];
+				if (role.startsWith(rolePrefix)) {
+					roles[index] = role.substring(rolePrefix.length());
+				}
 			}
 		}
-		return false;
+		return isGranted(this.authorizationManagerFactory.hasAnyRole(roles));
 	}
 
 	@Override
@@ -135,33 +162,37 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
 
 	@Override
 	public final boolean permitAll() {
-		return true;
+		return isGranted(this.authorizationManagerFactory.permitAll());
 	}
 
 	@Override
 	public final boolean denyAll() {
-		return false;
+		return isGranted(this.authorizationManagerFactory.denyAll());
 	}
 
 	@Override
 	public final boolean isAnonymous() {
-		return this.trustResolver.isAnonymous(getAuthentication());
+		return isGranted(this.authorizationManagerFactory.anonymous());
 	}
 
 	@Override
 	public final boolean isAuthenticated() {
-		return this.trustResolver.isAuthenticated(getAuthentication());
+		return isGranted(this.authorizationManagerFactory.authenticated());
 	}
 
 	@Override
 	public final boolean isRememberMe() {
-		return this.trustResolver.isRememberMe(getAuthentication());
+		return isGranted(this.authorizationManagerFactory.rememberMe());
 	}
 
 	@Override
 	public final boolean isFullyAuthenticated() {
-		Authentication authentication = getAuthentication();
-		return this.trustResolver.isFullyAuthenticated(authentication);
+		return isGranted(this.authorizationManagerFactory.fullyAuthenticated());
+	}
+
+	private boolean isGranted(AuthorizationManager<T> authorizationManager) {
+		AuthorizationResult authorizationResult = authorizationManager.authorize(this.authentication, this.object);
+		return (authorizationResult != null && authorizationResult.isGranted());
 	}
 
 	/**
@@ -173,12 +204,24 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
 		return getAuthentication().getPrincipal();
 	}
 
+	/**
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
+	 */
+	@Deprecated(since = "7.0")
 	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
-		this.trustResolver = trustResolver;
+		getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver);
 	}
 
+	/**
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
+	 */
+	@Deprecated(since = "7.0")
 	public void setRoleHierarchy(@Nullable RoleHierarchy roleHierarchy) {
-		this.roleHierarchy = roleHierarchy;
+		if (roleHierarchy != null) {
+			getDefaultAuthorizationManagerFactory().setRoleHierarchy(roleHierarchy);
+		}
 	}
 
 	/**
@@ -193,20 +236,46 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
 	 * If null or empty, then no default role prefix is used.
 	 * </p>
 	 * @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
-	public void setDefaultRolePrefix(String defaultRolePrefix) {
+	@Deprecated(since = "7.0")
+	public void setDefaultRolePrefix(@Nullable String defaultRolePrefix) {
+		if (defaultRolePrefix == null) {
+			defaultRolePrefix = "";
+		}
+		getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix);
 		this.defaultRolePrefix = defaultRolePrefix;
 	}
 
-	private Set<String> getAuthoritySet() {
-		if (this.roles == null) {
-			Collection<? extends GrantedAuthority> userAuthorities = getAuthentication().getAuthorities();
-			if (this.roleHierarchy != null) {
-				userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
-			}
-			this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
+	/**
+	 * Sets the {@link AuthorizationManagerFactory} to use for creating instances of
+	 * {@link AuthorizationManager}.
+	 * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} to use
+	 * @since 7.0
+	 */
+	public void setAuthorizationManagerFactory(AuthorizationManagerFactory<T> authorizationManagerFactory) {
+		Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory cannot be null");
+		this.authorizationManagerFactory = authorizationManagerFactory;
+	}
+
+	/**
+	 * Allows accessing the {@link DefaultAuthorizationManagerFactory} for getting and
+	 * setting defaults. This method will be removed in Spring Security 8.
+	 * @return the {@link DefaultAuthorizationManagerFactory}
+	 * @throws IllegalStateException if a different {@link AuthorizationManagerFactory}
+	 * was already set
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
+	 */
+	@Deprecated(since = "7.0", forRemoval = true)
+	private DefaultAuthorizationManagerFactory<T> getDefaultAuthorizationManagerFactory() {
+		if (!(this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory<T> defaultAuthorizationManagerFactory)) {
+			throw new IllegalStateException(
+					"authorizationManagerFactory must be an instance of DefaultAuthorizationManagerFactory");
 		}
-		return this.roles;
+
+		return defaultAuthorizationManagerFactory;
 	}
 
 	@Override
@@ -225,24 +294,4 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
 		this.permissionEvaluator = permissionEvaluator;
 	}
 
-	/**
-	 * Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role
-	 * does not already start with defaultRolePrefix.
-	 * @param defaultRolePrefix
-	 * @param role
-	 * @return
-	 */
-	private static String getRoleWithDefaultPrefix(@Nullable String defaultRolePrefix, String role) {
-		if (role == null) {
-			return role;
-		}
-		if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
-			return role;
-		}
-		if (role.startsWith(defaultRolePrefix)) {
-			return role;
-		}
-		return defaultRolePrefix + role;
-	}
-
 }

+ 29 - 6
core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java

@@ -43,6 +43,7 @@ import org.springframework.security.access.expression.AbstractSecurityExpression
 import org.springframework.security.access.expression.ExpressionUtils;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
 import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.parameters.DefaultSecurityParameterNameDiscoverer;
 import org.springframework.util.Assert;
@@ -56,11 +57,14 @@ import org.springframework.util.Assert;
  * @author Luke Taylor
  * @author Evgeniy Cheban
  * @author Blagoja Stamatovski
+ * @author Steve Riesenberg
  * @since 3.0
  */
 public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler<MethodInvocation>
 		implements MethodSecurityExpressionHandler {
 
+	private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
+
 	protected final Log logger = LogFactory.getLog(getClass());
 
 	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@@ -69,7 +73,7 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
 
 	private @Nullable PermissionCacheOptimizer permissionCacheOptimizer = null;
 
-	private String defaultRolePrefix = "ROLE_";
+	private String defaultRolePrefix = DEFAULT_ROLE_PREFIX;
 
 	public DefaultMethodSecurityExpressionHandler() {
 	}
@@ -106,12 +110,14 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
 
 	private MethodSecurityExpressionOperations createSecurityExpressionRoot(
 			Supplier<? extends @Nullable Authentication> authentication, MethodInvocation invocation) {
-		MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication);
+		MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication, invocation);
 		root.setThis(invocation.getThis());
+		root.setAuthorizationManagerFactory(getAuthorizationManagerFactory());
 		root.setPermissionEvaluator(getPermissionEvaluator());
-		root.setTrustResolver(getTrustResolver());
-		Optional.ofNullable(getRoleHierarchy()).ifPresent(root::setRoleHierarchy);
-		root.setDefaultRolePrefix(getDefaultRolePrefix());
+		if (!DEFAULT_ROLE_PREFIX.equals(this.defaultRolePrefix)) {
+			// Ensure SecurityExpressionRoot can strip the custom role prefix
+			root.setDefaultRolePrefix(getDefaultRolePrefix());
+		}
 		return root;
 	}
 
@@ -232,15 +238,22 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
 	 * {@link AuthenticationTrustResolverImpl}.
 	 * @param trustResolver the {@link AuthenticationTrustResolver} to use. Cannot be
 	 * null.
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
 		Assert.notNull(trustResolver, "trustResolver cannot be null");
+		getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver);
 		this.trustResolver = trustResolver;
 	}
 
 	/**
 	 * @return The current {@link AuthenticationTrustResolver}
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	protected AuthenticationTrustResolver getTrustResolver() {
 		return this.trustResolver;
 	}
@@ -289,14 +302,24 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
 	 * If null or empty, then no default role prefix is used.
 	 * </p>
 	 * @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
-	public void setDefaultRolePrefix(String defaultRolePrefix) {
+	@Deprecated(since = "7.0")
+	public void setDefaultRolePrefix(@Nullable String defaultRolePrefix) {
+		if (defaultRolePrefix == null) {
+			defaultRolePrefix = "";
+		}
+		getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix);
 		this.defaultRolePrefix = defaultRolePrefix;
 	}
 
 	/**
 	 * @return The default role prefix
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	protected String getDefaultRolePrefix() {
 		return this.defaultRolePrefix;
 	}

+ 7 - 7
core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java

@@ -18,6 +18,7 @@ package org.springframework.security.access.expression.method;
 
 import java.util.function.Supplier;
 
+import org.aopalliance.intercept.MethodInvocation;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.access.expression.SecurityExpressionRoot;
@@ -28,9 +29,11 @@ import org.springframework.security.core.Authentication;
  *
  * @author Luke Taylor
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 3.0
  */
-class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
+class MethodSecurityExpressionRoot extends SecurityExpressionRoot<MethodInvocation>
+		implements MethodSecurityExpressionOperations {
 
 	private @Nullable Object filterObject;
 
@@ -38,12 +41,9 @@ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements Met
 
 	private @Nullable Object target;
 
-	MethodSecurityExpressionRoot(@Nullable Authentication a) {
-		super(a);
-	}
-
-	MethodSecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication) {
-		super(authentication);
+	MethodSecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication,
+			MethodInvocation methodInvocation) {
+		super(authentication, methodInvocation);
 	}
 
 	@Override

+ 124 - 0
core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright 2002-present 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;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A factory for creating different kinds of {@link AuthorizationManager} instances.
+ *
+ * @param <T> the type of object that the authorization check is being done on
+ * @author Steve Riesenberg
+ * @since 7.0
+ */
+public interface AuthorizationManagerFactory<T extends @Nullable Object> {
+
+	/**
+	 * Create an {@link AuthorizationManager} that allows anyone.
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> permitAll() {
+		return SingleResultAuthorizationManager.permitAll();
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that does not allow anyone.
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> denyAll() {
+		return SingleResultAuthorizationManager.denyAll();
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that requires users to have the specified
+	 * role.
+	 * @param role the role (automatically prepended with ROLE_) that should be required
+	 * to allow access (i.e. USER, ADMIN, etc.)
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> hasRole(String role) {
+		return AuthorityAuthorizationManager.hasRole(role);
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that requires users to have one of many
+	 * roles.
+	 * @param roles the roles (automatically prepended with ROLE_) that the user should
+	 * have at least one of to allow access (i.e. USER, ADMIN, etc.)
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> hasAnyRole(String... roles) {
+		return AuthorityAuthorizationManager.hasAnyRole(roles);
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that requires users to have the specified
+	 * authority.
+	 * @param authority the authority that should be required to allow access (i.e.
+	 * ROLE_USER, ROLE_ADMIN, etc.)
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> hasAuthority(String authority) {
+		return AuthorityAuthorizationManager.hasAuthority(authority);
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that requires users to have one of many
+	 * authorities.
+	 * @param authorities the authorities that the user should have at least one of to
+	 * allow access (i.e. ROLE_USER, ROLE_ADMIN, etc.)
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> hasAnyAuthority(String... authorities) {
+		return AuthorityAuthorizationManager.hasAnyAuthority(authorities);
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that allows any authenticated user.
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> authenticated() {
+		return AuthenticatedAuthorizationManager.authenticated();
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that allows users who have authenticated
+	 * and were not remembered.
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> fullyAuthenticated() {
+		return AuthenticatedAuthorizationManager.fullyAuthenticated();
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that allows users that have been
+	 * remembered.
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> rememberMe() {
+		return AuthenticatedAuthorizationManager.rememberMe();
+	}
+
+	/**
+	 * Creates an {@link AuthorizationManager} that allows only anonymous users.
+	 * @return A new {@link AuthorizationManager} instance
+	 */
+	default AuthorizationManager<T> anonymous() {
+		return AuthenticatedAuthorizationManager.anonymous();
+	}
+
+}

+ 123 - 0
core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright 2002-present 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;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.authentication.AuthenticationTrustResolver;
+import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.util.Assert;
+
+/**
+ * A factory for creating different kinds of {@link AuthorizationManager} instances.
+ *
+ * @param <T> the type of object that the authorization check is being done on
+ * @author Steve Riesenberg
+ * @since 7.0
+ */
+public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object>
+		implements AuthorizationManagerFactory<T> {
+
+	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
+
+	private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
+
+	private String rolePrefix = "ROLE_";
+
+	/**
+	 * Sets the {@link AuthenticationTrustResolver} used to check the user's
+	 * authentication.
+	 * @param trustResolver the {@link AuthenticationTrustResolver} to use
+	 */
+	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
+		Assert.notNull(trustResolver, "trustResolver cannot be null");
+		this.trustResolver = trustResolver;
+	}
+
+	/**
+	 * Sets the {@link RoleHierarchy} used to discover reachable authorities.
+	 * @param roleHierarchy the {@link RoleHierarchy} to use
+	 */
+	public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
+		Assert.notNull(roleHierarchy, "roleHierarchy cannot be null");
+		this.roleHierarchy = roleHierarchy;
+	}
+
+	/**
+	 * Sets the prefix used to create an authority name from a role name. Can be an empty
+	 * string.
+	 * @param rolePrefix the role prefix to use
+	 */
+	public void setRolePrefix(String rolePrefix) {
+		Assert.notNull(rolePrefix, "rolePrefix cannot be null");
+		this.rolePrefix = rolePrefix;
+	}
+
+	@Override
+	public AuthorizationManager<T> hasRole(String role) {
+		return hasAnyRole(role);
+	}
+
+	@Override
+	public AuthorizationManager<T> hasAnyRole(String... roles) {
+		return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles));
+	}
+
+	@Override
+	public AuthorizationManager<T> hasAuthority(String authority) {
+		return withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority));
+	}
+
+	@Override
+	public AuthorizationManager<T> hasAnyAuthority(String... authorities) {
+		return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities));
+	}
+
+	@Override
+	public AuthorizationManager<T> authenticated() {
+		return withTrustResolver(AuthenticatedAuthorizationManager.authenticated());
+	}
+
+	@Override
+	public AuthorizationManager<T> fullyAuthenticated() {
+		return withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated());
+	}
+
+	@Override
+	public AuthorizationManager<T> rememberMe() {
+		return withTrustResolver(AuthenticatedAuthorizationManager.rememberMe());
+	}
+
+	@Override
+	public AuthorizationManager<T> anonymous() {
+		return withTrustResolver(AuthenticatedAuthorizationManager.anonymous());
+	}
+
+	private AuthorityAuthorizationManager<T> withRoleHierarchy(AuthorityAuthorizationManager<T> authorizationManager) {
+		authorizationManager.setRoleHierarchy(this.roleHierarchy);
+		return authorizationManager;
+	}
+
+	private AuthenticatedAuthorizationManager<T> withTrustResolver(
+			AuthenticatedAuthorizationManager<T> authorizationManager) {
+		authorizationManager.setTrustResolver(this.trustResolver);
+		return authorizationManager;
+	}
+
+}

+ 2 - 1
core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRootTests.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.access.expression.method;
 
+import org.aopalliance.intercept.MethodInvocation;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -53,7 +54,7 @@ public class MethodSecurityExpressionRootTests {
 	@BeforeEach
 	public void createContext() {
 		this.user = mock(Authentication.class);
-		this.root = new MethodSecurityExpressionRoot(this.user);
+		this.root = new MethodSecurityExpressionRoot(() -> this.user, mock(MethodInvocation.class));
 		this.ctx = new StandardEvaluationContext();
 		this.ctx.setRootObject(this.root);
 		this.trustResolver = mock(AuthenticationTrustResolver.class);

+ 100 - 0
core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright 2002-present 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;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link AuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+public class AuthorizationManagerFactoryTests {
+
+	@Test
+	public void permitAllReturnsSingleResultAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.permitAll();
+		assertThat(authorizationManager).isInstanceOf(SingleResultAuthorizationManager.class);
+	}
+
+	@Test
+	public void denyAllReturnsSingleResultAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.denyAll();
+		assertThat(authorizationManager).isInstanceOf(SingleResultAuthorizationManager.class);
+	}
+
+	@Test
+	public void hasRoleReturnsAuthorityAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.hasRole("USER");
+		assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
+	}
+
+	@Test
+	public void hasAnyRoleReturnsAuthorityAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.hasAnyRole("USER", "ADMIN");
+		assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
+	}
+
+	@Test
+	public void hasAuthorityReturnsAuthorityAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.hasAuthority("authority1");
+		assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
+	}
+
+	@Test
+	public void hasAnyAuthorityReturnsAuthorityAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.hasAnyAuthority("authority1", "authority2");
+		assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
+	}
+
+	@Test
+	public void authenticatedReturnsAuthenticatedAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.authenticated();
+		assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class);
+	}
+
+	@Test
+	public void fullyAuthenticatedReturnsAuthenticatedAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.fullyAuthenticated();
+		assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class);
+	}
+
+	@Test
+	public void rememberMeReturnsAuthenticatedAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.rememberMe();
+		assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class);
+	}
+
+	@Test
+	public void anonymousReturnsAuthenticatedAuthorizationManagerByDefault() {
+		AuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		AuthorizationManager<String> authorizationManager = factory.anonymous();
+		assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class);
+	}
+
+}

+ 53 - 11
data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java

@@ -26,6 +26,8 @@ import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
 import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -92,18 +94,18 @@ import org.springframework.util.Assert;
  */
 public class SecurityEvaluationContextExtension implements EvaluationContextExtension {
 
+	private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
+
 	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
 		.getContextHolderStrategy();
 
 	private @Nullable Authentication authentication;
 
-	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
-
-	private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
+	private AuthorizationManagerFactory<Object> authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>();
 
 	private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator();
 
-	private String defaultRolePrefix = "ROLE_";
+	private String defaultRolePrefix = DEFAULT_ROLE_PREFIX;
 
 	/**
 	 * Creates a new instance that uses the current {@link Authentication} found on the
@@ -126,14 +128,16 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte
 	}
 
 	@Override
-	public SecurityExpressionRoot getRootObject() {
+	public SecurityExpressionRoot<Object> getRootObject() {
 		Authentication authentication = getAuthentication();
-		SecurityExpressionRoot root = new SecurityExpressionRoot(authentication) {
+		SecurityExpressionRoot<Object> root = new SecurityExpressionRoot<>(() -> authentication, new Object()) {
 		};
-		root.setTrustResolver(this.trustResolver);
-		root.setRoleHierarchy(this.roleHierarchy);
+		root.setAuthorizationManagerFactory(this.authorizationManagerFactory);
 		root.setPermissionEvaluator(this.permissionEvaluator);
-		root.setDefaultRolePrefix(this.defaultRolePrefix);
+		if (!DEFAULT_ROLE_PREFIX.equals(this.defaultRolePrefix)) {
+			// Ensure SecurityExpressionRoot can strip the custom role prefix
+			root.setDefaultRolePrefix(this.defaultRolePrefix);
+		}
 		return root;
 	}
 
@@ -156,15 +160,46 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte
 		return context.getAuthentication();
 	}
 
+	/**
+	 * Sets the {@link AuthorizationManagerFactory} to be used. The default is
+	 * {@link DefaultAuthorizationManagerFactory}.
+	 * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} to use.
+	 * Cannot be null.
+	 * @since 7.0
+	 */
+	public void setAuthorizationManagerFactory(AuthorizationManagerFactory<Object> authorizationManagerFactory) {
+		Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory cannot be null");
+		this.authorizationManagerFactory = authorizationManagerFactory;
+	}
+
+	/**
+	 * Allows accessing the {@link DefaultAuthorizationManagerFactory} for getting and
+	 * setting defaults. This method will be removed in Spring Security 8.
+	 * @return the {@link DefaultAuthorizationManagerFactory}
+	 * @throws IllegalStateException if a different {@link AuthorizationManagerFactory}
+	 * was already set
+	 */
+	private DefaultAuthorizationManagerFactory<Object> getDefaultAuthorizationManagerFactory() {
+		if (!(this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory<Object> defaultAuthorizationManagerFactory)) {
+			throw new IllegalStateException(
+					"authorizationManagerFactory must be an instance of DefaultAuthorizationManagerFactory");
+		}
+
+		return defaultAuthorizationManagerFactory;
+	}
+
 	/**
 	 * Sets the {@link AuthenticationTrustResolver} to be used. Default is
 	 * {@link AuthenticationTrustResolverImpl}. Cannot be null.
 	 * @param trustResolver the {@link AuthenticationTrustResolver} to use
 	 * @since 5.8
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
 		Assert.notNull(trustResolver, "trustResolver cannot be null");
-		this.trustResolver = trustResolver;
+		getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver);
 	}
 
 	/**
@@ -172,10 +207,13 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte
 	 * Cannot be null.
 	 * @param roleHierarchy the {@link RoleHierarchy} to use
 	 * @since 5.8
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
 		Assert.notNull(roleHierarchy, "roleHierarchy cannot be null");
-		this.roleHierarchy = roleHierarchy;
+		getDefaultAuthorizationManagerFactory().setRoleHierarchy(roleHierarchy);
 	}
 
 	/**
@@ -199,8 +237,12 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte
 	 * @param defaultRolePrefix the default prefix to add to roles. The default is
 	 * "ROLE_".
 	 * @since 5.8
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setDefaultRolePrefix(String defaultRolePrefix) {
+		getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix);
 		this.defaultRolePrefix = defaultRolePrefix;
 	}
 

+ 16 - 15
data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java

@@ -23,10 +23,9 @@ import org.junit.jupiter.api.Test;
 import org.springframework.security.access.PermissionEvaluator;
 import org.springframework.security.access.expression.DenyAllPermissionEvaluator;
 import org.springframework.security.access.expression.SecurityExpressionRoot;
-import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
-import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -102,9 +101,11 @@ public class SecurityEvaluationContextExtensionTests {
 	public void setTrustResolverWhenNotNullThenVerifyRootObject() {
 		TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
 		this.securityExtension = new SecurityEvaluationContextExtension(explicit);
-		AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
+		AuthenticationTrustResolver trustResolver = mock(AuthenticationTrustResolver.class);
+		given(trustResolver.isAuthenticated(explicit)).willReturn(true);
 		this.securityExtension.setTrustResolver(trustResolver);
-		assertThat(getRoot()).extracting("trustResolver").isEqualTo(trustResolver);
+		assertThat(getRoot().isAuthenticated()).isTrue();
+		verify(trustResolver).isAuthenticated(explicit);
 	}
 
 	@Test
@@ -117,11 +118,11 @@ public class SecurityEvaluationContextExtensionTests {
 
 	@Test
 	public void setRoleHierarchyWhenNotNullThenVerifyRootObject() {
-		TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
+		TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_PARENT");
 		this.securityExtension = new SecurityEvaluationContextExtension(explicit);
-		RoleHierarchy roleHierarchy = new NullRoleHierarchy();
+		RoleHierarchy roleHierarchy = RoleHierarchyImpl.fromHierarchy("ROLE_PARENT > ROLE_EXPLICIT");
 		this.securityExtension.setRoleHierarchy(roleHierarchy);
-		assertThat(getRoot()).extracting("roleHierarchy").isEqualTo(roleHierarchy);
+		assertThat(getRoot().hasRole("EXPLICIT")).isTrue();
 	}
 
 	@Test
@@ -143,25 +144,25 @@ public class SecurityEvaluationContextExtensionTests {
 
 	@Test
 	public void setDefaultRolePrefixWhenCustomThenVerifyRootObject() {
-		TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
+		TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "CUSTOM_EXPLICIT");
 		this.securityExtension = new SecurityEvaluationContextExtension(explicit);
 		String defaultRolePrefix = "CUSTOM_";
 		this.securityExtension.setDefaultRolePrefix(defaultRolePrefix);
-		assertThat(getRoot()).extracting("defaultRolePrefix").isEqualTo(defaultRolePrefix);
+		assertThat(getRoot().hasRole("EXPLICIT")).isTrue();
 	}
 
 	@Test
 	public void getRootObjectWhenAdditionalFieldsNotSetThenVerifyDefaults() {
 		TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
 		this.securityExtension = new SecurityEvaluationContextExtension(explicit);
-		SecurityExpressionRoot root = getRoot();
-		assertThat(root).extracting("trustResolver").isInstanceOf(AuthenticationTrustResolverImpl.class);
-		assertThat(root).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class);
-		assertThat(root).extracting("permissionEvaluator").isInstanceOf(DenyAllPermissionEvaluator.class);
-		assertThat(root).extracting("defaultRolePrefix").isEqualTo("ROLE_");
+		SecurityExpressionRoot<?> securityExpressionRoot = getRoot();
+		assertThat(securityExpressionRoot.isAuthenticated()).isTrue();
+		assertThat(securityExpressionRoot.hasRole("PARENT")).isFalse();
+		assertThat(securityExpressionRoot.hasRole("EXPLICIT")).isTrue();
+		assertThat(securityExpressionRoot.hasPermission(new Object(), "read")).isFalse();
 	}
 
-	private SecurityExpressionRoot getRoot() {
+	private SecurityExpressionRoot<?> getRoot() {
 		return this.securityExtension.getRootObject();
 	}
 

+ 37 - 0
docs/modules/ROOT/pages/servlet/authorization/architecture.adoc

@@ -144,6 +144,43 @@ 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-authorization-manager-factory]]
+=== Creating AuthorizationManager instances
+
+The javadoc:org.springframework.security.authorization.AuthorizationManagerFactory[] interface (introduced in Spring Security 7.0) is used to create generic ``AuthorizationManager``s in xref:servlet/authorization/authorize-http-requests.adoc[request-based] and xref:servlet/authorization/method-security.adoc[method-based] authorization components.
+The following is a sketch of the `AuthorizationManagerFactory` interface:
+
+[source,java]
+----
+public interface AuthorizationManagerFactory<T> {
+	AuthorizationManager<T> permitAll();
+	AuthorizationManager<T> denyAll();
+	AuthorizationManager<T> hasRole(String role);
+	AuthorizationManager<T> hasAnyRole(String... roles);
+	AuthorizationManager<T> hasAuthority(String authority);
+	AuthorizationManager<T> hasAnyAuthority(String... authorities);
+	AuthorizationManager<T> authenticated();
+	AuthorizationManager<T> fullyAuthenticated();
+	AuthorizationManager<T> rememberMe();
+	AuthorizationManager<T> anonymous();
+}
+----
+
+The default implementation is javadoc:org.springframework.security.authorization.DefaultAuthorizationManagerFactory[], which allows for customizing the `rolePrefix` (defaults to `"ROLE_"`), `RoleHierarchy` and `AuthenticationTrustManager` that are provided to the ``AuthorizationManager``s created by the factory.
+
+In order to customize the default instance used by Spring Security, simply publish a bean as in the following example:
+
+include-code::./AuthorizationManagerFactoryConfiguration[tag=config,indent=0]
+
+[TIP]
+It is also possible to target a specific usage of this factory within Spring Security by providing a concrete parameterized type instead of a generic type.
+See examples of each in the xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] sections of the documentation.
+
+In addition to simply customizing the default instance of `AuthorizationManagerFactory`, you can provide your own implementation to fully customize the instances created by the factory and provide your own implementations.
+
+[NOTE]
+The {gh-url}/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java[actual interface] provides default implementations for all factory methods, which allows custom implementations to only implement the methods that need to be customized.
+
 [[authz-authorization-managers]]
 ==== AuthorizationManagers
 There are also helpful static factories in javadoc:org.springframework.security.authorization.AuthorizationManagers[] for composing individual ``AuthorizationManager``s into more sophisticated expressions.

+ 19 - 0
docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc

@@ -57,6 +57,7 @@ In many cases, your authorization rules will be more sophisticated than that, so
 * I want to <<match-by-custom, match a request programmatically>>
 * I want to <<authorize-requests, authorize a request programmatically>>
 * I want to <<remote-authorization-manager, delegate request authorization>> to a policy agent
+* I want to <<customizing-authorization-managers,customize how authorization managers are created>>
 
 [[request-authorization-architecture]]
 == Understanding How Request Authorization Components Work
@@ -765,6 +766,24 @@ You will notice that since we are using the `hasRole` expression we do not need
 <6> 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.
 
+[[customizing-authorization-managers]]
+== Customizing Authorization Managers
+
+When you use the `authorizeHttpRequests` DSL, Spring Security takes care of creating the appropriate `AuthorizationManager` instances for you.
+In certain cases, you may want to customize what is created in order to have complete control over how authorization decisions are made xref:servlet/authorization/architecture.adoc#authz-delegate-authorization-manager[at the framework level].
+
+In order to take control of creating instances of `AuthorizationManager` for authorizing HTTP requests, you can create a custom xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`].
+For example, let's say you want to create a convention that authenticated users must be authenticated _AND_ have the `USER` role.
+To do this, you can create a custom implementation for HTTP requests as in the following example:
+
+include-code::./CustomHttpRequestsAuthorizationManagerFactory[tag=class,indent=0]
+
+Now, whenever you <<activate-request-security,require authentication>>, Spring Security will automatically invoke your custom factory to create an instance of `AuthorizationManager` that requires authentication _AND_ the `USER` role.
+
+[TIP]
+We use this as a simple example of creating a custom `AuthorizationManagerFactory`, though it is also possible (and often simpler) to replace a specific `AuthorizationManager` only for a particular request.
+See <<remote-authorization-manager>> for an example.
+
 [[authorization-expressions]]
 == Expressing Authorization with SpEL
 

+ 18 - 0
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -1995,6 +1995,24 @@ This works on both classes and interfaces.
 This does not work for interfaces, since they do not have debug information about the parameter names.
 For interfaces, either annotations or the `-parameters` approach must be used.
 
+[[customizing-authorization-managers]]
+== Customizing Authorization Managers
+
+When you use SpEL expressions with <<use-preauthorize,`@PreAuthorize`>>, <<use-postauthorize,`@PostAuthorize`>>, <<use-prefilter,`@PreFilter`>> and <<use-postfilter,`@PostFilter`>>, Spring Security takes care of creating the appropriate `AuthorizationManager` instances for you.
+In certain cases, you may want to customize what is created in order to have complete control over how authorization decisions are made xref:servlet/authorization/architecture.adoc#authz-delegate-authorization-manager[at the framework level].
+
+In order to take control of creating instances of `AuthorizationManager` for pre- and post-annotations, you can create a custom xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`].
+For example, let's say you want to allow users with the `ADMIN` role whenever any other role is required.
+To do this, you can create a custom implementation for method security as in the following example:
+
+include-code::./CustomMethodInvocationAuthorizationManagerFactory[tag=class,indent=0]
+
+Now, whenever you <<use-preauthorize,use the `@PreAuthorize` annotation>> with `hasRole` or `hasAnyRole`, Spring Security will automatically invoke your custom factory to create an instance of `AuthorizationManager` that allows access for the given role(s) _OR_ the `ADMIN` role.
+
+[TIP]
+We use this as a simple example of creating a custom `AuthorizationManagerFactory`, though the same outcome could be accomplished with <<favor-granting-authorities,a role hierarchy>>.
+Use whichever approach fits best in your situation.
+
 [[authorize-object]]
 == Authorizing Arbitrary Objects
 

+ 1 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -12,6 +12,7 @@ Each section that follows will indicate the more notable removals as well as the
 == Core
 
 * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
+* Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components
 
 == Config
 

+ 77 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfiguration.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2004-present 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 clients 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.docs.servlet.authorization.authzauthorizationmanagerfactory;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
+import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
+
+/**
+ * Documentation for {@link AuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+@Configuration(proxyBeanMethods = false)
+public class AuthorizationManagerFactoryConfiguration {
+
+	// tag::config[]
+	@Bean
+	<T> AuthorizationManagerFactory<T> authorizationManagerFactory() {
+		DefaultAuthorizationManagerFactory<T> authorizationManagerFactory =
+				new DefaultAuthorizationManagerFactory<>();
+		authorizationManagerFactory.setTrustResolver(getAuthenticationTrustResolver());
+		authorizationManagerFactory.setRoleHierarchy(getRoleHierarchy());
+		authorizationManagerFactory.setRolePrefix("role_");
+
+		return authorizationManagerFactory;
+	}
+	// end::config[]
+
+	private static AuthenticationTrustResolverImpl getAuthenticationTrustResolver() {
+		AuthenticationTrustResolverImpl authenticationTrustResolver =
+				new AuthenticationTrustResolverImpl();
+		authenticationTrustResolver.setAnonymousClass(Anonymous.class);
+		authenticationTrustResolver.setRememberMeClass(RememberMe.class);
+
+		return authenticationTrustResolver;
+	}
+
+	private static RoleHierarchyImpl getRoleHierarchy() {
+		return RoleHierarchyImpl.fromHierarchy("role_admin > role_user");
+	}
+
+	static class Anonymous extends TestingAuthenticationToken {
+
+		Anonymous(String principal) {
+			super(principal, "", "role_anonymous");
+		}
+
+	}
+
+	static class RememberMe extends TestingAuthenticationToken {
+
+		RememberMe(String principal) {
+			super(principal, "", "role_rememberMe");
+		}
+
+	}
+
+}

+ 208 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfigurationTests.java

@@ -0,0 +1,208 @@
+/*
+ * Copyright 2004-present 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 clients 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.docs.servlet.authorization.authzauthorizationmanagerfactory;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.docs.servlet.authorization.authzauthorizationmanagerfactory.AuthorizationManagerFactoryConfiguration.Anonymous;
+import org.springframework.security.docs.servlet.authorization.authzauthorizationmanagerfactory.AuthorizationManagerFactoryConfiguration.RememberMe;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link AuthorizationManagerFactoryConfiguration}.
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class AuthorizationManagerFactoryConfigurationTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@Test
+	void getAnonymousWhenCustomAnonymousClassThenOk() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new Anonymous("anonymous");
+		// @formatter:off
+		this.mockMvc.perform(get("/anonymous").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getAnonymousWhenAuthenticatedThenForbidden() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "role_user");
+		// @formatter:off
+		this.mockMvc.perform(get("/anonymous").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getRememberMeWhenCustomRememberMeClassThenOk() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new RememberMe("rememberMe");
+		// @formatter:off
+		this.mockMvc.perform(get("/rememberMe").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getRememberMeWhenAuthenticatedThenForbidden() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "role_user");
+		// @formatter:off
+		this.mockMvc.perform(get("/rememberMe").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getUserWhenCustomUserRoleThenOk() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "role_user");
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getUserWhenCustomAdminRoleThenOk() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("admin", "", "role_admin");
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getPreAuthorizeWhenCustomUserRoleThenOk() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "role_user");
+		// @formatter:off
+		this.mockMvc.perform(get("/preAuthorize").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getPreAuthorizeWhenCustomAdminRoleThenOk() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("admin", "", "role_admin");
+		// @formatter:off
+		this.mockMvc.perform(get("/preAuthorize").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getPreAuthorizeWhenOtherRoleThenForbidden() throws Exception {
+		this.spring.register(AuthorizationManagerFactoryConfiguration.class, SecurityConfiguration.class,
+				TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("other", "", "role_other");
+		// @formatter:off
+		this.mockMvc.perform(get("/preAuthorize").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@EnableWebMvc
+	@EnableWebSecurity
+	@EnableMethodSecurity
+	@Configuration
+	static class SecurityConfiguration {
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize
+					.requestMatchers("/anonymous").anonymous()
+					.requestMatchers("/rememberMe").rememberMe()
+					.requestMatchers("/user").hasRole("user")
+					.requestMatchers("/preAuthorize").permitAll()
+					.anyRequest().denyAll()
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@RestController
+	static class TestController {
+
+		@GetMapping({ "/anonymous", "/rememberMe", "/user" })
+		@ResponseStatus(HttpStatus.OK)
+		void httpRequest() {
+		}
+
+		@GetMapping("/preAuthorize")
+		@ResponseStatus(HttpStatus.OK)
+		@PreAuthorize("hasRole('user')")
+		void preAuthorize() {
+		}
+
+	}
+
+}

+ 48 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactory.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2004-present 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 clients 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.docs.servlet.authorization.customizingauthorizationmanagers;
+
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
+import org.springframework.security.authorization.AuthorizationManagers;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
+import org.springframework.stereotype.Component;
+
+/**
+ * Documentation for {@link AuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+public class CustomHttpRequestsAuthorizationManagerFactory
+		implements AuthorizationManagerFactory<RequestAuthorizationContext> {
+
+	private final AuthorizationManagerFactory<RequestAuthorizationContext> delegate =
+			new DefaultAuthorizationManagerFactory<>();
+
+	@Override
+	public AuthorizationManager<RequestAuthorizationContext> authenticated() {
+		return AuthorizationManagers.allOf(
+			this.delegate.authenticated(),
+			this.delegate.hasRole("USER")
+		);
+	}
+
+}
+// end::class[]

+ 137 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactoryTests.java

@@ -0,0 +1,137 @@
+/*
+ * Copyright 2004-present 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 clients 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.docs.servlet.authorization.customizingauthorizationmanagers;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link CustomHttpRequestsAuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class CustomHttpRequestsAuthorizationManagerFactoryTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@Test
+	void getHelloWhenAnonymousThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/hello").with(anonymous()))
+			.andExpect(status().isForbidden())
+			.andExpect(unauthenticated());
+		// @formatter:on
+	}
+
+	@Test
+	void getHelloWhenAuthenticatedWithNoRolesThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", Collections.emptyList());
+		// @formatter:off
+		this.mockMvc.perform(get("/hello").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getHelloWhenAuthenticatedWithUserRoleThenOk() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "ROLE_USER");
+		// @formatter:off
+		this.mockMvc.perform(get("/hello").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getHelloWhenAuthenticatedWithOtherRoleThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "ROLE_OTHER");
+		// @formatter:off
+		this.mockMvc.perform(get("/hello").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@EnableWebMvc
+	@EnableWebSecurity
+	@Configuration
+	static class SecurityConfiguration {
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize
+					.anyRequest().authenticated()
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		CustomHttpRequestsAuthorizationManagerFactory customHttpRequestsAuthorizationManagerFactory() {
+			return new CustomHttpRequestsAuthorizationManagerFactory();
+		}
+
+	}
+
+	@RestController
+	static class TestController {
+
+		@GetMapping("/**")
+		@ResponseStatus(HttpStatus.OK)
+		void ok() {
+		}
+
+	}
+
+}

+ 57 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactory.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2004-present 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 clients 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.docs.servlet.authorization.customizingauthorizationmanagers;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
+import org.springframework.security.authorization.AuthorizationManagers;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Documentation for {@link AuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+public class CustomMethodInvocationAuthorizationManagerFactory
+		implements AuthorizationManagerFactory<MethodInvocation> {
+
+	private final AuthorizationManagerFactory<MethodInvocation> delegate =
+			new DefaultAuthorizationManagerFactory<>();
+
+	@Override
+	public AuthorizationManager<MethodInvocation> hasRole(String role) {
+		return AuthorizationManagers.anyOf(
+			this.delegate.hasRole(role),
+			this.delegate.hasRole("ADMIN")
+		);
+	}
+
+	@Override
+	public AuthorizationManager<MethodInvocation> hasAnyRole(String... roles) {
+		return AuthorizationManagers.anyOf(
+			this.delegate.hasAnyRole(roles),
+			this.delegate.hasRole("ADMIN")
+		);
+	}
+
+}
+// end::class[]

+ 191 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactoryTests.java

@@ -0,0 +1,191 @@
+/*
+ * Copyright 2004-present 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 clients 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.docs.servlet.authorization.customizingauthorizationmanagers;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link CustomMethodInvocationAuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class CustomMethodInvocationAuthorizationManagerFactoryTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@Test
+	void getUserWhenAnonymousThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(anonymous()))
+			.andExpect(status().isForbidden())
+			.andExpect(unauthenticated());
+		// @formatter:on
+	}
+
+	@Test
+	void getUserWhenAuthenticatedWithNoRolesThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", Collections.emptyList());
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getUserWhenAuthenticatedWithUserRoleThenOk() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "ROLE_USER");
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getUserWhenAuthenticatedWithAdminRoleThenOk() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("admin", "", "ROLE_ADMIN");
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getUserWhenAuthenticatedWithOtherRoleThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "ROLE_OTHER");
+		// @formatter:off
+		this.mockMvc.perform(get("/user").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getRolesWhenAuthenticatedWithRole1RoleThenOk() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "ROLE_ROLE1");
+		// @formatter:off
+		this.mockMvc.perform(get("/roles").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getRolesWhenAuthenticatedWithAdminRoleThenOk() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("admin", "", "ROLE_ADMIN");
+		// @formatter:off
+		this.mockMvc.perform(get("/roles").with(authentication(authentication)))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@Test
+	void getRolesWhenAuthenticatedWithOtherRoleThenForbidden() throws Exception {
+		this.spring.register(SecurityConfiguration.class, TestController.class).autowire();
+		Authentication authentication = new TestingAuthenticationToken("user", "", "ROLE_OTHER");
+		// @formatter:off
+		this.mockMvc.perform(get("/roles").with(authentication(authentication)))
+			.andExpect(status().isForbidden())
+			.andExpect(authenticated().withAuthentication(authentication));
+		// @formatter:on
+	}
+
+	@EnableWebMvc
+	@EnableWebSecurity
+	@EnableMethodSecurity
+	@Configuration
+	static class SecurityConfiguration {
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize
+					.anyRequest().authenticated()
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		CustomMethodInvocationAuthorizationManagerFactory customMethodInvocationAuthorizationManagerFactory() {
+			return new CustomMethodInvocationAuthorizationManagerFactory();
+		}
+
+	}
+
+	@RestController
+	static class TestController {
+
+		@GetMapping("/user")
+		@ResponseStatus(HttpStatus.OK)
+		@PreAuthorize("hasRole('USER')")
+		void user() {
+		}
+
+		@GetMapping("/roles")
+		@ResponseStatus(HttpStatus.OK)
+		@PreAuthorize("hasAnyRole('ROLE1', 'ROLE2')")
+		void roles() {
+		}
+
+	}
+
+}

+ 62 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfiguration.kt

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2004-present 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.kt.docs.servlet.authorization.authzauthorizationmanagerfactory
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl
+import org.springframework.security.authentication.AuthenticationTrustResolverImpl
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.authorization.AuthorizationManagerFactory
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
+
+/**
+ * Documentation for [org.springframework.security.authorization.AuthorizationManagerFactory].
+ *
+ * @author Steve Riesenberg
+ */
+@Configuration(proxyBeanMethods = false)
+class AuthorizationManagerFactoryConfiguration {
+    // tag::config[]
+    @Bean
+    fun <T> authorizationManagerFactory(): AuthorizationManagerFactory<T> {
+        val authorizationManagerFactory = DefaultAuthorizationManagerFactory<T>()
+        authorizationManagerFactory.setTrustResolver(getAuthenticationTrustResolver())
+        authorizationManagerFactory.setRoleHierarchy(getRoleHierarchy())
+        authorizationManagerFactory.setRolePrefix("role_")
+
+        return authorizationManagerFactory
+    }
+    // end::config[]
+
+    private fun getAuthenticationTrustResolver(): AuthenticationTrustResolverImpl {
+        val authenticationTrustResolver = AuthenticationTrustResolverImpl()
+        authenticationTrustResolver.setAnonymousClass(Anonymous::class.java)
+        authenticationTrustResolver.setRememberMeClass(RememberMe::class.java)
+
+        return authenticationTrustResolver
+    }
+
+    private fun getRoleHierarchy(): RoleHierarchyImpl {
+        return RoleHierarchyImpl.fromHierarchy("role_admin > role_user")
+    }
+
+    internal class Anonymous(principal: String) :
+        TestingAuthenticationToken(principal, "", "role_anonymous")
+
+    internal class RememberMe(principal: String) :
+        TestingAuthenticationToken(principal, "", "role_rememberMe")
+}

+ 229 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfigurationTests.kt

@@ -0,0 +1,229 @@
+/*
+ * Copyright 2004-present 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.kt.docs.servlet.authorization.authzauthorizationmanagerfactory
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.kt.docs.servlet.authorization.authzauthorizationmanagerfactory.AuthorizationManagerFactoryConfiguration.Anonymous
+import org.springframework.security.kt.docs.servlet.authorization.authzauthorizationmanagerfactory.AuthorizationManagerFactoryConfiguration.RememberMe
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.ResponseStatus
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ * Tests for [AuthorizationManagerFactoryConfiguration].
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class AuthorizationManagerFactoryConfigurationTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    @Throws(Exception::class)
+    fun getAnonymousWhenCustomAnonymousClassThenOk() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = Anonymous("anonymous")
+        // @formatter:off
+        mockMvc.perform(get("/anonymous").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getAnonymousWhenAuthenticatedThenForbidden() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = TestingAuthenticationToken("user", "", "role_user")
+        // @formatter:off
+        mockMvc.perform(get("/anonymous").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getRememberMeWhenCustomRememberMeClassThenOk() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = RememberMe("rememberMe")
+        // @formatter:off
+        mockMvc.perform(get("/rememberMe").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getRememberMeWhenAuthenticatedThenForbidden() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val user = TestingAuthenticationToken("user", "", "role_user")
+        // @formatter:off
+        mockMvc.perform(get("/rememberMe").with(authentication(user)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(user))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenCustomUserRoleThenOk() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = TestingAuthenticationToken("user", "", "role_user")
+        // @formatter:off
+        mockMvc.perform(get("/user").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenCustomAdminRoleThenOk() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val admin = TestingAuthenticationToken("admin", "", "role_admin")
+        // @formatter:off
+        mockMvc.perform(get("/user").with(authentication(admin)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(admin))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getPreAuthorizeWhenCustomUserRoleThenOk() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = TestingAuthenticationToken("user", "", "role_user")
+        // @formatter:off
+        mockMvc.perform(get("/preAuthorize").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getPreAuthorizeWhenCustomAdminRoleThenOk() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = TestingAuthenticationToken("admin", "", "role_admin")
+        // @formatter:off
+        mockMvc.perform(get("/preAuthorize").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getPreAuthorizeWhenOtherRoleThenForbidden() {
+        this.spring.register(AuthorizationManagerFactoryConfiguration::class.java, SecurityConfiguration::class.java)
+            .autowire()
+        val authentication = TestingAuthenticationToken("other", "", "role_other")
+        // @formatter:off
+        mockMvc.perform(get("/preAuthorize").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @EnableWebMvc
+    @EnableWebSecurity
+    @EnableMethodSecurity
+    @Configuration
+    internal open class SecurityConfiguration {
+        @Bean
+        @Throws(Exception::class)
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            // @formatter:off
+            http.authorizeHttpRequests { authorize ->
+                authorize
+                    .requestMatchers("/anonymous").anonymous()
+                    .requestMatchers("/rememberMe").rememberMe()
+                    .requestMatchers("/user").hasRole("user")
+                    .requestMatchers("/preAuthorize").permitAll()
+                    .anyRequest().denyAll()
+            }
+            // @formatter:on
+            return http.build()
+        }
+
+        @Bean
+        open fun testController(testService: TestService): TestController {
+            return TestController(testService())
+        }
+
+        @Bean
+        open fun testService(): TestService {
+            return TestServiceImpl()
+        }
+    }
+
+    @RestController
+    internal open class TestController(private val testService: TestService) {
+        @GetMapping(value = ["/anonymous", "/rememberMe", "/user"])
+        @ResponseStatus(HttpStatus.OK)
+        fun httpRequest() {
+        }
+
+        @GetMapping("/preAuthorize")
+        @ResponseStatus(HttpStatus.OK)
+        fun preAuthorize() {
+            testService.preAuthorize()
+        }
+    }
+
+    internal interface TestService {
+        @PreAuthorize("hasRole('user')")
+        fun preAuthorize()
+    }
+
+    internal open class TestServiceImpl : TestService {
+        override fun preAuthorize() {
+        }
+    }
+}

+ 44 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactory.kt

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2004-present 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.kt.docs.servlet.authorization.customizingauthorizationmanagers
+
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.authorization.AuthorizationManagerFactory
+import org.springframework.security.authorization.AuthorizationManagers
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
+import org.springframework.stereotype.Component
+
+/**
+ * Documentation for {@link AuthorizationManagerFactory}.
+ *
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+class CustomHttpRequestsAuthorizationManagerFactory : AuthorizationManagerFactory<RequestAuthorizationContext> {
+    private val delegate = DefaultAuthorizationManagerFactory<RequestAuthorizationContext>()
+
+    override fun authenticated(): AuthorizationManager<RequestAuthorizationContext> {
+        return AuthorizationManagers.allOf(
+            delegate.authenticated(),
+            delegate.hasRole("USER")
+        )
+    }
+}
+// end::class[]
+
+

+ 131 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomHttpRequestsAuthorizationManagerFactoryTests.kt

@@ -0,0 +1,131 @@
+/*
+ * Copyright 2004-present 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.kt.docs.servlet.authorization.customizingauthorizationmanagers
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.ResponseStatus
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ * Tests for [CustomHttpRequestsAuthorizationManagerFactory].
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class CustomHttpRequestsAuthorizationManagerFactoryTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    @Throws(Exception::class)
+    fun getHelloWhenAnonymousThenForbidden() {
+        spring.register(SecurityConfiguration::class.java, TestController::class.java).autowire()
+        // @formatter:off
+        mockMvc.perform(get("/hello").with(anonymous()))
+            .andExpect(status().isForbidden())
+            .andExpect(SecurityMockMvcResultMatchers.unauthenticated())
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getHelloWhenAuthenticatedWithUserRoleThenOk() {
+        spring.register(SecurityConfiguration::class.java, TestController::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_USER")
+        // @formatter:off
+        mockMvc.perform(get("/hello").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getHelloWhenAuthenticatedWithOtherRoleThenForbidden() {
+        spring.register(SecurityConfiguration::class.java, TestController::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_OTHER")
+        // @formatter:off
+        mockMvc.perform(get("/hello").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getHelloWhenAuthenticatedWithNoRolesThenForbidden() {
+        spring.register(SecurityConfiguration::class.java, TestController::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", listOf())
+        // @formatter:off
+        mockMvc.perform(get("/hello").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @EnableWebMvc
+    @EnableWebSecurity
+    @Configuration
+    internal open class SecurityConfiguration {
+        @Bean
+        @Throws(Exception::class)
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            // @formatter:off
+            http
+                .authorizeHttpRequests { authorize ->
+                    authorize.anyRequest().authenticated()
+                }
+            // @formatter:on
+            return http.build()
+        }
+
+        @Bean
+        open fun customHttpRequestsAuthorizationManagerFactory(): CustomHttpRequestsAuthorizationManagerFactory {
+            return CustomHttpRequestsAuthorizationManagerFactory()
+        }
+    }
+
+    @RestController
+    internal class TestController {
+        @GetMapping("/**")
+        @ResponseStatus(HttpStatus.OK)
+        fun ok() {
+        }
+    }
+}

+ 50 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactory.kt

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2004-present 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.kt.docs.servlet.authorization.customizingauthorizationmanagers
+
+import org.aopalliance.intercept.MethodInvocation
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.authorization.AuthorizationManagerFactory
+import org.springframework.security.authorization.AuthorizationManagers
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
+import org.springframework.stereotype.Component
+
+/**
+ * Documentation for [AuthorizationManagerFactory].
+ *
+ * @author Steve Riesenberg
+ */
+// tag::class[]
+@Component
+class CustomMethodInvocationAuthorizationManagerFactory : AuthorizationManagerFactory<MethodInvocation> {
+    private val delegate = DefaultAuthorizationManagerFactory<MethodInvocation>()
+
+    override fun hasRole(role: String): AuthorizationManager<MethodInvocation> {
+        return AuthorizationManagers.anyOf(
+            delegate.hasRole(role),
+            delegate.hasRole("ADMIN")
+        )
+    }
+
+    override fun hasAnyRole(vararg roles: String): AuthorizationManager<MethodInvocation> {
+        return AuthorizationManagers.anyOf(
+            delegate.hasAnyRole(*roles),
+            delegate.hasRole("ADMIN")
+        )
+    }
+}
+// end::class[]
+

+ 215 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authorization/customizingauthorizationmanagers/CustomMethodInvocationAuthorizationManagerFactoryTests.kt

@@ -0,0 +1,215 @@
+/*
+ * Copyright 2004-present 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.kt.docs.servlet.authorization.customizingauthorizationmanagers
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.ResponseStatus
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ * Tests for [CustomMethodInvocationAuthorizationManagerFactory].
+ *
+ * @author Steve Riesenberg
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class CustomMethodInvocationAuthorizationManagerFactoryTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenAnonymousThenForbidden() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        // @formatter:off
+        mockMvc.perform(get("/user").with(anonymous()))
+            .andExpect(status().isForbidden())
+            .andExpect(unauthenticated())
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenAuthenticatedWithNoRolesThenForbidden() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", listOf())
+        // @formatter:off
+        mockMvc.perform(get("/user").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenAuthenticatedWithUserRoleThenOk() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_USER")
+        // @formatter:off
+        mockMvc.perform(get("/user").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenAuthenticatedWithAdminRoleThenOk() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_ADMIN")
+        // @formatter:off
+        mockMvc.perform(get("/user").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getUserWhenAuthenticatedWithOtherRoleThenForbidden() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_OTHER")
+        // @formatter:off
+        mockMvc.perform(get("/user").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getRolesWhenAuthenticatedWithRole1RoleThenOk() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_ROLE1")
+        // @formatter:off
+        mockMvc.perform(get("/roles").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getRolesWhenAuthenticatedWithAdminRoleThenOk() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_ADMIN")
+        // @formatter:off
+        mockMvc.perform(get("/roles").with(authentication(authentication)))
+            .andExpect(status().isOk())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getRolesWhenAuthenticatedWithOtherRoleThenForbidden() {
+        spring.register(SecurityConfiguration::class.java).autowire()
+        val authentication = TestingAuthenticationToken("user", "", "ROLE_OTHER")
+        // @formatter:off
+        mockMvc.perform(get("/roles").with(authentication(authentication)))
+            .andExpect(status().isForbidden())
+            .andExpect(authenticated().withAuthentication(authentication))
+        // @formatter:on
+    }
+
+    @EnableWebMvc
+    @EnableWebSecurity
+    @EnableMethodSecurity
+    @Configuration
+    internal open class SecurityConfiguration {
+        @Bean
+        @Throws(Exception::class)
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            // @formatter:off
+            http
+                .authorizeHttpRequests { authorize ->
+                    authorize.anyRequest().authenticated()
+                }
+            // @formatter:on
+            return http.build()
+        }
+
+        @Bean
+        open fun customMethodInvocationAuthorizationManagerFactory(): CustomMethodInvocationAuthorizationManagerFactory {
+            return CustomMethodInvocationAuthorizationManagerFactory()
+        }
+
+        @Bean
+        open fun testController(testService: TestService): TestController {
+            return TestController(testService())
+        }
+
+        @Bean
+        open fun testService(): TestService {
+            return TestServiceImpl()
+        }
+    }
+
+    @RestController
+    internal open class TestController(private val testService: TestService) {
+        @GetMapping("/user")
+        @ResponseStatus(HttpStatus.OK)
+        fun user() {
+            testService.user()
+        }
+
+        @GetMapping("/roles")
+        @ResponseStatus(HttpStatus.OK)
+        fun roles() {
+            testService.roles()
+        }
+    }
+
+    internal interface TestService {
+        @PreAuthorize("hasRole('USER')")
+        fun user()
+
+        @PreAuthorize("hasAnyRole('ROLE1', 'ROLE2')")
+        fun roles()
+    }
+
+    internal open class TestServiceImpl : TestService {
+        override fun user() {
+        }
+
+        override fun roles() {
+        }
+    }
+}

+ 40 - 0
docs/src/test/resources/org/springframework/security/docs/servlet/authorization/authzauthorizationmanagerfactory/AuthorizationManagerFactoryConfiguration.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2004-present the original author or authors.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xsi:schemaLocation="
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<!-- tag::config[] -->
+	<b:bean id="authorizationManagerFactory" class="org.springframework.security.authorization.DefaultAuthorizationManagerFactory">
+		<b:property name="trustResolver" ref="authenticationTrustResolver"/>
+		<b:property name="roleHierarchy" ref="roleHierarchy"/>
+		<b:property name="rolePrefix" value="role_"/>
+	</b:bean>
+	<!-- end::config[] -->
+
+	<b:bean id="authenticationTrustResolver" class="org.springframework.security.authentication.AuthenticationTrustResolverImpl">
+		<b:property name="anonymousClass" value="org.springframework.security.authentication.TestingAuthenticationToken"/>
+	</b:bean>
+
+	<b:bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy">
+		<b:constructor-arg name="hierarchy" value="role_admin > role_user"/>
+	</b:bean>
+
+</b:beans>

+ 1 - 0
etc/checkstyle/checkstyle-suppressions.xml

@@ -36,6 +36,7 @@
 	<suppress files="JoseHeader\.java" checks="SpringMethodVisibility"/>
 	<suppress files="DefaultLoginPageGeneratingFilterTests\.java" checks="SpringLeadingWhitespace"/>
 	<suppress files="AuthenticationException\.java" checks="MutableException"/>
+	<suppress files="FilterInvocationExpressionRoot\.java" checks="SpringMethodVisibility"/>
 
 	<!-- Lambdas that we can't replace with a method reference because a closure is required -->
 	<suppress files="BearerTokenAuthenticationFilter\.java" checks="SpringLambda"/>

+ 11 - 11
messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java

@@ -28,9 +28,8 @@ import org.springframework.security.access.expression.AbstractSecurityExpression
 import org.springframework.security.access.expression.SecurityExpressionHandler;
 import org.springframework.security.access.expression.SecurityExpressionOperations;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
-import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
-import org.springframework.util.Assert;
 
 /**
  * The default implementation of {@link SecurityExpressionHandler} which uses a
@@ -43,12 +42,10 @@ import org.springframework.util.Assert;
  */
 public class DefaultMessageSecurityExpressionHandler<T> extends AbstractSecurityExpressionHandler<Message<T>> {
 
-	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
-
 	@Override
 	public EvaluationContext createEvaluationContext(Supplier<? extends @Nullable Authentication> authentication,
 			Message<T> message) {
-		MessageSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, message);
+		MessageSecurityExpressionRoot<T> root = createSecurityExpressionRoot(authentication, message);
 		StandardEvaluationContext ctx = new StandardEvaluationContext(root);
 		BeanResolver beanResolver = getBeanResolver();
 		if (beanResolver != null) {
@@ -64,18 +61,21 @@ public class DefaultMessageSecurityExpressionHandler<T> extends AbstractSecurity
 		return createSecurityExpressionRoot(() -> authentication, invocation);
 	}
 
-	private MessageSecurityExpressionRoot createSecurityExpressionRoot(
+	private MessageSecurityExpressionRoot<T> createSecurityExpressionRoot(
 			Supplier<? extends Authentication> authentication, Message<T> invocation) {
-		MessageSecurityExpressionRoot root = new MessageSecurityExpressionRoot(authentication, invocation);
+		MessageSecurityExpressionRoot<T> root = new MessageSecurityExpressionRoot<>(authentication, invocation);
+		root.setAuthorizationManagerFactory(getAuthorizationManagerFactory());
 		root.setPermissionEvaluator(getPermissionEvaluator());
-		root.setTrustResolver(this.trustResolver);
-		root.setRoleHierarchy(getRoleHierarchy());
 		return root;
 	}
 
+	/**
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
+	 */
+	@Deprecated(since = "7.0")
 	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
-		Assert.notNull(trustResolver, "trustResolver cannot be null");
-		this.trustResolver = trustResolver;
+		getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver);
 	}
 
 }

+ 9 - 5
messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java

@@ -18,6 +18,8 @@ package org.springframework.security.messaging.access.expression;
 
 import java.util.function.Supplier;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.messaging.Message;
 import org.springframework.security.access.expression.SecurityExpressionRoot;
 import org.springframework.security.core.Authentication;
@@ -27,13 +29,14 @@ import org.springframework.security.core.Authentication;
  *
  * @author Rob Winch
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 4.0
  */
-public class MessageSecurityExpressionRoot extends SecurityExpressionRoot {
+public class MessageSecurityExpressionRoot<T> extends SecurityExpressionRoot<Message<T>> {
 
-	public final Message<?> message;
+	public final Message<T> message;
 
-	public MessageSecurityExpressionRoot(Authentication authentication, Message<?> message) {
+	public MessageSecurityExpressionRoot(Authentication authentication, Message<T> message) {
 		this(() -> authentication, message);
 	}
 
@@ -44,8 +47,9 @@ public class MessageSecurityExpressionRoot extends SecurityExpressionRoot {
 	 * @param message the {@link Message} to use
 	 * @since 5.8
 	 */
-	public MessageSecurityExpressionRoot(Supplier<? extends Authentication> authentication, Message<?> message) {
-		super(authentication);
+	public MessageSecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication,
+			Message<T> message) {
+		super(authentication, message);
 		this.message = message;
 	}
 

+ 18 - 8
web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java

@@ -27,6 +27,7 @@ import org.springframework.security.access.expression.SecurityExpressionHandler;
 import org.springframework.security.access.expression.SecurityExpressionOperations;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
 import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
 import org.springframework.util.Assert;
@@ -36,14 +37,15 @@ import org.springframework.util.Assert;
  * create a {@link WebSecurityExpressionRoot}.
  *
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 5.8
  */
 public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpressionHandler<RequestAuthorizationContext>
 		implements SecurityExpressionHandler<RequestAuthorizationContext> {
 
-	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
+	private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
 
-	private String defaultRolePrefix = "ROLE_";
+	private String defaultRolePrefix = DEFAULT_ROLE_PREFIX;
 
 	@Override
 	@SuppressWarnings("NullAway") // https://github.com/spring-projects/spring-framework/issues/35371
@@ -64,11 +66,13 @@ public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpres
 
 	private WebSecurityExpressionRoot createSecurityExpressionRoot(
 			Supplier<? extends @Nullable Authentication> authentication, RequestAuthorizationContext context) {
-		WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, context.getRequest());
-		root.setRoleHierarchy(getRoleHierarchy());
+		WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, context);
+		root.setAuthorizationManagerFactory(getAuthorizationManagerFactory());
 		root.setPermissionEvaluator(getPermissionEvaluator());
-		root.setTrustResolver(this.trustResolver);
-		root.setDefaultRolePrefix(this.defaultRolePrefix);
+		if (!DEFAULT_ROLE_PREFIX.equals(this.defaultRolePrefix)) {
+			// Ensure SecurityExpressionRoot can strip the custom role prefix
+			root.setDefaultRolePrefix(this.defaultRolePrefix);
+		}
 		return root;
 	}
 
@@ -76,10 +80,12 @@ public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpres
 	 * Sets the {@link AuthenticationTrustResolver} to be used. The default is
 	 * {@link AuthenticationTrustResolverImpl}.
 	 * @param trustResolver the {@link AuthenticationTrustResolver} to use
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
-		Assert.notNull(trustResolver, "trustResolver cannot be null");
-		this.trustResolver = trustResolver;
+		getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver);
 	}
 
 	/**
@@ -91,9 +97,13 @@ public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpres
 	 * role ROLE_ADMIN will be used when the defaultRolePrefix is "ROLE_" (default).
 	 * @param defaultRolePrefix the default prefix to add to roles. The default is
 	 * "ROLE_".
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setDefaultRolePrefix(String defaultRolePrefix) {
 		Assert.notNull(defaultRolePrefix, "defaultRolePrefix cannot be null");
+		getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix);
 		this.defaultRolePrefix = defaultRolePrefix;
 	}
 

+ 22 - 10
web/src/main/java/org/springframework/security/web/access/expression/DefaultWebSecurityExpressionHandler.java

@@ -23,30 +23,33 @@ import org.springframework.security.access.expression.SecurityExpressionHandler;
 import org.springframework.security.access.expression.SecurityExpressionOperations;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
 import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
+import org.springframework.security.authorization.AuthorizationManagerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.FilterInvocation;
-import org.springframework.util.Assert;
 
 /**
  * @author Luke Taylor
  * @author Eddú Meléndez
+ * @author Steve Riesenberg
  * @since 3.0
  */
 public class DefaultWebSecurityExpressionHandler extends AbstractSecurityExpressionHandler<FilterInvocation>
 		implements SecurityExpressionHandler<FilterInvocation> {
 
-	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
+	private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
 
-	private String defaultRolePrefix = "ROLE_";
+	private String defaultRolePrefix = DEFAULT_ROLE_PREFIX;
 
 	@Override
 	protected SecurityExpressionOperations createSecurityExpressionRoot(@Nullable Authentication authentication,
 			FilterInvocation fi) {
-		WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, fi);
+		FilterInvocationExpressionRoot root = new FilterInvocationExpressionRoot(() -> authentication, fi);
+		root.setAuthorizationManagerFactory(getAuthorizationManagerFactory());
 		root.setPermissionEvaluator(getPermissionEvaluator());
-		root.setTrustResolver(this.trustResolver);
-		root.setRoleHierarchy(getRoleHierarchy());
-		root.setDefaultRolePrefix(this.defaultRolePrefix);
+		if (!DEFAULT_ROLE_PREFIX.equals(this.defaultRolePrefix)) {
+			// Ensure SecurityExpressionRoot can strip the custom role prefix
+			root.setDefaultRolePrefix(this.defaultRolePrefix);
+		}
 		return root;
 	}
 
@@ -55,10 +58,12 @@ public class DefaultWebSecurityExpressionHandler extends AbstractSecurityExpress
 	 * {@link AuthenticationTrustResolverImpl}.
 	 * @param trustResolver the {@link AuthenticationTrustResolver} to use. Cannot be
 	 * null.
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
+	@Deprecated(since = "7.0")
 	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
-		Assert.notNull(trustResolver, "trustResolver cannot be null");
-		this.trustResolver = trustResolver;
+		getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver);
 	}
 
 	/**
@@ -75,8 +80,15 @@ public class DefaultWebSecurityExpressionHandler extends AbstractSecurityExpress
 	 * If null or empty, then no default role prefix is used.
 	 * </p>
 	 * @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
+	 * @deprecated Use
+	 * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead
 	 */
-	public void setDefaultRolePrefix(String defaultRolePrefix) {
+	@Deprecated(since = "7.0")
+	public void setDefaultRolePrefix(@Nullable String defaultRolePrefix) {
+		if (defaultRolePrefix == null) {
+			defaultRolePrefix = "";
+		}
+		getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix);
 		this.defaultRolePrefix = defaultRolePrefix;
 	}
 

+ 62 - 0
web/src/main/java/org/springframework/security/web/access/expression/FilterInvocationExpressionRoot.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.access.expression;
+
+import java.util.function.Supplier;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.security.access.expression.SecurityExpressionRoot;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.util.matcher.IpAddressMatcher;
+
+/**
+ * @author Steve Riesenberg
+ * @since 7.0
+ */
+final class FilterInvocationExpressionRoot extends SecurityExpressionRoot<FilterInvocation> {
+
+	/**
+	 * Allows direct access to the request object
+	 */
+	public final HttpServletRequest request;
+
+	/**
+	 * Creates an instance for the given {@link Supplier} of the {@link Authentication}
+	 * and {@link HttpServletRequest}.
+	 * @param authentication the {@link Supplier} of the {@link Authentication} to use
+	 * @param fi the {@link FilterInvocation} to use
+	 */
+	FilterInvocationExpressionRoot(Supplier<Authentication> authentication, FilterInvocation fi) {
+		super(authentication, fi);
+		this.request = fi.getRequest();
+	}
+
+	/**
+	 * Takes a specific IP address or a range using the IP/Netmask (e.g. 192.168.1.0/24 or
+	 * 202.24.0.0/14).
+	 * @param ipAddress the address or range of addresses from which the request must
+	 * come.
+	 * @return true if the IP address of the current request is in the required range.
+	 */
+	public boolean hasIpAddress(String ipAddress) {
+		IpAddressMatcher matcher = new IpAddressMatcher(ipAddress);
+		return matcher.matches(this.request);
+	}
+
+}

+ 26 - 3
web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java

@@ -24,22 +24,29 @@ import org.jspecify.annotations.Nullable;
 import org.springframework.security.access.expression.SecurityExpressionRoot;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
 import org.springframework.security.web.util.matcher.IpAddressMatcher;
 
 /**
  * @author Luke Taylor
  * @author Evgeniy Cheban
+ * @author Steve Riesenberg
  * @since 3.0
  */
-public class WebSecurityExpressionRoot extends SecurityExpressionRoot {
+public class WebSecurityExpressionRoot extends SecurityExpressionRoot<RequestAuthorizationContext> {
 
 	/**
 	 * Allows direct access to the request object
 	 */
 	public final HttpServletRequest request;
 
+	/**
+	 * @deprecated Use
+	 * {@link #WebSecurityExpressionRoot(Supplier, RequestAuthorizationContext)} instead
+	 */
+	@Deprecated(since = "7.0")
 	public WebSecurityExpressionRoot(@Nullable Authentication a, FilterInvocation fi) {
-		this(() -> a, fi.getRequest());
+		this(() -> a, new RequestAuthorizationContext(fi.getRequest()));
 	}
 
 	/**
@@ -48,14 +55,30 @@ public class WebSecurityExpressionRoot extends SecurityExpressionRoot {
 	 * @param authentication the {@link Supplier} of the {@link Authentication} to use
 	 * @param request the {@link HttpServletRequest} to use
 	 * @since 5.8
+	 * @deprecated Use
+	 * {@link #WebSecurityExpressionRoot(Supplier, RequestAuthorizationContext)} instead
 	 */
+	@Deprecated(since = "7.0")
 	@SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1246
 	public WebSecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication,
 			HttpServletRequest request) {
-		super(authentication);
+		super(authentication, new RequestAuthorizationContext(request));
 		this.request = request;
 	}
 
+	/**
+	 * Creates an instance for the given {@link Supplier} of the {@link Authentication}
+	 * and {@link HttpServletRequest}.
+	 * @param authentication the {@link Supplier} of the {@link Authentication} to use
+	 * @param context the {@link RequestAuthorizationContext} to use
+	 * @since 7.0
+	 */
+	public WebSecurityExpressionRoot(Supplier<? extends @Nullable Authentication> authentication,
+			RequestAuthorizationContext context) {
+		super(authentication, context);
+		this.request = context.getRequest();
+	}
+
 	/**
 	 * Takes a specific IP address or a range using the IP/Netmask (e.g. 192.168.1.0/24 or
 	 * 202.24.0.0/14).