mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-31 06:38:42 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			554 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			554 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| [[jc-erms]]
 | |
| = EnableReactiveMethodSecurity
 | |
| 
 | |
| Spring Security supports method security by using https://projectreactor.io/docs/core/release/reference/#context[Reactor's Context], which is set up by `ReactiveSecurityContextHolder`.
 | |
| The following example shows how to retrieve the currently logged in user's message:
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| For this example to work, the return type of the method must be a `org.reactivestreams.Publisher` (that is, a `Mono` or a `Flux`).
 | |
| This is necessary to integrate with Reactor's `Context`.
 | |
| ====
 | |
| 
 | |
| [[jc-enable-reactive-method-security-authorization-manager]]
 | |
| == EnableReactiveMethodSecurity with AuthorizationManager
 | |
| 
 | |
| In Spring Security 5.8, we can enable annotation-based security using the `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` annotation on any `@Configuration` instance.
 | |
| 
 | |
| This improves upon `@EnableReactiveMethodSecurity` in a number of ways. `@EnableReactiveMethodSecurity(useAuthorizationManager=true)`:
 | |
| 
 | |
| 1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
 | |
| This simplifies reuse and customization.
 | |
| 2. Supports reactive return types including Kotlin coroutines.
 | |
| 3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
 | |
| 4. Checks for conflicting annotations to ensure an unambiguous security configuration
 | |
| 5. Complies with JSR-250
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| For earlier versions, please read about similar support with <<jc-enable-reactive-method-security, @EnableReactiveMethodSecurity>>.
 | |
| ====
 | |
| 
 | |
| For example, the following would enable Spring Security's `@PreAuthorize` annotation:
 | |
| 
 | |
| .Method Security Configuration
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @EnableReactiveMethodSecurity(useAuthorizationManager=true)
 | |
| public class MethodSecurityConfig {
 | |
| 	// ...
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
 | |
| Spring Security's native annotation support defines a set of attributes for the method.
 | |
| These will be passed to the various method interceptors, like `AuthorizationManagerBeforeReactiveMethodInterceptor`, for it to make the actual decision:
 | |
| 
 | |
| .Method Security Annotation Usage
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| public interface BankService {
 | |
| 	@PreAuthorize("hasRole('USER')")
 | |
| 	Mono<Account> readAccount(Long id);
 | |
| 
 | |
| 	@PreAuthorize("hasRole('USER')")
 | |
| 	Flux<Account> findAccounts();
 | |
| 
 | |
| 	@PreAuthorize("@func.apply(#account)")
 | |
| 	Mono<Account> post(Account account, Double amount);
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| In this case `hasRole` refers to the method found in `SecurityExpressionRoot` that gets invoked by the SpEL evaluation engine.
 | |
| 
 | |
| `@bean` refers to a custom component you have defined, where `apply` can return `Boolean` or `Mono<Boolean>` to indicate the authorization decision.
 | |
| A bean like that might look something like this:
 | |
| 
 | |
| .Method Security Reactive Boolean Expression
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| public Function<Account, Mono<Boolean>> func() {
 | |
|     return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| === Customizing Authorization
 | |
| 
 | |
| Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support.
 | |
| 
 | |
| 
 | |
| [[jc-reactive-method-security-custom-granted-authority-defaults]]
 | |
| Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`.
 | |
| You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so:
 | |
| 
 | |
| .Custom MethodSecurityExpressionHandler
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| static GrantedAuthorityDefaults grantedAuthorityDefaults() {
 | |
| 	return new GrantedAuthorityDefaults("MYPREFIX_");
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| [TIP]
 | |
| ====
 | |
| We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
 | |
| Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
 | |
| ====
 | |
| 
 | |
| [[jc-reactive-method-security-custom-authorization-manager]]
 | |
| === Custom Authorization Managers
 | |
| 
 | |
| Method authorization is a combination of before- and after-method authorization.
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| Before-method authorization is performed before the method is invoked.
 | |
| If that authorization denies access, the method is not invoked, and an `AccessDeniedException` is thrown.
 | |
| After-method authorization is performed after the method is invoked, but before the method returns to the caller.
 | |
| If that authorization denies access, the value is not returned, and an `AccessDeniedException` is thrown
 | |
| ====
 | |
| 
 | |
| To recreate what adding `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` does by default, you would publish the following configuration:
 | |
| 
 | |
| .Full Pre-post Method Security Configuration
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Configuration
 | |
| class MethodSecurityConfig {
 | |
| 	@Bean
 | |
| 	BeanDefinitionRegistryPostProcessor aopConfig() {
 | |
| 		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor() {
 | |
| 		return new PreFilterAuthorizationReactiveMethodInterceptor();
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor() {
 | |
| 		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize();
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor() {
 | |
| 		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize();
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor() {
 | |
| 		return new PostFilterAuthorizationReactiveMethodInterceptor();
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| Notice that Spring Security's method security is built using Spring AOP.
 | |
| So, interceptors are invoked based on the order specified.
 | |
| This can be customized by calling `setOrder` on the interceptor instances like so:
 | |
| 
 | |
| .Publish Custom Advisor
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| Advisor postFilterAuthorizationMethodInterceptor() {
 | |
| 	PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
 | |
| 	interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
 | |
| 	return interceptor;
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| You may want to only support `@PreAuthorize` in your application, in which case you can do the following:
 | |
| 
 | |
| .Only @PreAuthorize Configuration
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Configuration
 | |
| class MethodSecurityConfig {
 | |
| 	@Bean
 | |
| 	BeanDefinitionRegistryPostProcessor aopConfig() {
 | |
| 		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	Advisor preAuthorize() {
 | |
| 		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| Or, you may have a custom before-method `ReactiveAuthorizationManager` that you want to add to the list.
 | |
| 
 | |
| In this case, you will need to tell Spring Security both the `ReactiveAuthorizationManager` and to which methods and classes your authorization manager applies.
 | |
| 
 | |
| Thus, you can configure Spring Security to invoke your `ReactiveAuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so:
 | |
| 
 | |
| .Custom Before Advisor
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @EnableReactiveMethodSecurity(useAuthorizationManager=true)
 | |
| class MethodSecurityConfig {
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	public Advisor customAuthorize() {
 | |
| 		JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
 | |
| 		pattern.setPattern("org.mycompany.myapp.service.*");
 | |
| 		ReactiveAuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
 | |
| 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule);
 | |
| 		interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
 | |
| 		return interceptor;
 | |
|     }
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| [TIP]
 | |
| ====
 | |
| You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
 | |
| ====
 | |
| 
 | |
| The same can be done for after-method authorization.
 | |
| After-method authorization is generally concerned with analysing the return value to verify access.
 | |
| 
 | |
| For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
 | |
| 
 | |
| .@PostAuthorize example
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| public interface BankService {
 | |
| 
 | |
| 	@PreAuthorize("hasRole('USER')")
 | |
| 	@PostAuthorize("returnObject.owner == authentication.name")
 | |
| 	Mono<Account> readAccount(Long id);
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated.
 | |
| 
 | |
| For example, if you have your own custom annotation, you can configure it like so:
 | |
| 
 | |
| 
 | |
| .Custom After Advisor
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @EnableReactiveMethodSecurity(useAuthorizationManager=true)
 | |
| class MethodSecurityConfig {
 | |
| 	@Bean
 | |
| 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | |
| 	public Advisor customAuthorize(ReactiveAuthorizationManager<MethodInvocationResult> rules) {
 | |
| 		AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
 | |
| 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules);
 | |
| 		interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
 | |
| 		return interceptor;
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| and it will be invoked after the `@PostAuthorize` interceptor.
 | |
| 
 | |
| == EnableReactiveMethodSecurity
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
 | |
| 
 | |
| Mono<String> messageByUsername = ReactiveSecurityContextHolder.getContext()
 | |
| 	.map(SecurityContext::getAuthentication)
 | |
| 	.map(Authentication::getName)
 | |
| 	.flatMap(this::findMessageByUsername)
 | |
| 	// In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
 | |
| 	.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
 | |
| 
 | |
| StepVerifier.create(messageByUsername)
 | |
| 	.expectNext("Hi user")
 | |
| 	.verifyComplete();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER")
 | |
| 
 | |
| val messageByUsername: Mono<String> = ReactiveSecurityContextHolder.getContext()
 | |
| 	.map(SecurityContext::getAuthentication)
 | |
| 	.map(Authentication::getName)
 | |
| 	.flatMap(this::findMessageByUsername) // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
 | |
| 	.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))
 | |
| 
 | |
| StepVerifier.create(messageByUsername)
 | |
| 	.expectNext("Hi user")
 | |
| 	.verifyComplete()
 | |
| ----
 | |
| ======
 | |
| 
 | |
| Where `this::findMessageByUsername` is defined as:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| Mono<String> findMessageByUsername(String username) {
 | |
| 	return Mono.just("Hi " + username);
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| fun findMessageByUsername(username: String): Mono<String> {
 | |
| 	return Mono.just("Hi $username")
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| The following minimal method security configures method security in reactive applications:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Configuration
 | |
| @EnableReactiveMethodSecurity
 | |
| public class SecurityConfig {
 | |
| 	@Bean
 | |
| 	public MapReactiveUserDetailsService userDetailsService() {
 | |
| 		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
 | |
| 		UserDetails rob = userBuilder.username("rob")
 | |
| 			.password("rob")
 | |
| 			.roles("USER")
 | |
| 			.build();
 | |
| 		UserDetails admin = userBuilder.username("admin")
 | |
| 			.password("admin")
 | |
| 			.roles("USER","ADMIN")
 | |
| 			.build();
 | |
| 		return new MapReactiveUserDetailsService(rob, admin);
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Configuration
 | |
| @EnableReactiveMethodSecurity
 | |
| class SecurityConfig {
 | |
| 	@Bean
 | |
| 	fun userDetailsService(): MapReactiveUserDetailsService {
 | |
| 		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
 | |
| 		val rob = userBuilder.username("rob")
 | |
| 			.password("rob")
 | |
| 			.roles("USER")
 | |
| 			.build()
 | |
| 		val admin = userBuilder.username("admin")
 | |
| 			.password("admin")
 | |
| 			.roles("USER", "ADMIN")
 | |
| 			.build()
 | |
| 		return MapReactiveUserDetailsService(rob, admin)
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| Consider the following class:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Component
 | |
| public class HelloWorldMessageService {
 | |
| 	@PreAuthorize("hasRole('ADMIN')")
 | |
| 	public Mono<String> findMessage() {
 | |
| 		return Mono.just("Hello World!");
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Component
 | |
| class HelloWorldMessageService {
 | |
| 	@PreAuthorize("hasRole('ADMIN')")
 | |
| 	fun findMessage(): Mono<String> {
 | |
| 		return Mono.just("Hello World!")
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| Alternatively, the following class uses Kotlin coroutines:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="primary"]
 | |
| ----
 | |
| @Component
 | |
| class HelloWorldMessageService {
 | |
|     @PreAuthorize("hasRole('ADMIN')")
 | |
|     suspend fun findMessage(): String {
 | |
|         delay(10)
 | |
|         return "Hello World!"
 | |
|     }
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| 
 | |
| Combined with our configuration above, `@PreAuthorize("hasRole('ADMIN')")` ensures that `findByMessage` is invoked only by a user with the `ADMIN` role.
 | |
| Note that any of the expressions in standard method security work for `@EnableReactiveMethodSecurity`.
 | |
| However, at this time, we support only a return type of `Boolean` or `boolean` of the expression.
 | |
| This means that the expression must not block.
 | |
| 
 | |
| When integrating with xref:reactive/configuration/webflux.adoc#jc-webflux[WebFlux Security], the Reactor Context is automatically established by Spring Security according to the authenticated user:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Configuration
 | |
| @EnableWebFluxSecurity
 | |
| @EnableReactiveMethodSecurity
 | |
| public class SecurityConfig {
 | |
| 
 | |
| 	@Bean
 | |
| 	SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
 | |
| 		return http
 | |
| 			// Demonstrate that method security works
 | |
| 			// Best practice to use both for defense in depth
 | |
| 			.authorizeExchange(exchanges -> exchanges
 | |
| 				.anyExchange().permitAll()
 | |
| 			)
 | |
| 			.httpBasic(withDefaults())
 | |
| 			.build();
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	MapReactiveUserDetailsService userDetailsService() {
 | |
| 		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
 | |
| 		UserDetails rob = userBuilder.username("rob")
 | |
| 			.password("rob")
 | |
| 			.roles("USER")
 | |
| 			.build();
 | |
| 		UserDetails admin = userBuilder.username("admin")
 | |
| 			.password("admin")
 | |
| 			.roles("USER","ADMIN")
 | |
| 			.build();
 | |
| 		return new MapReactiveUserDetailsService(rob, admin);
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Configuration
 | |
| @EnableWebFluxSecurity
 | |
| @EnableReactiveMethodSecurity
 | |
| class SecurityConfig {
 | |
| 	@Bean
 | |
| 	open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
 | |
| 		return http {
 | |
| 			authorizeExchange {
 | |
| 				authorize(anyExchange, permitAll)
 | |
| 			}
 | |
| 			httpBasic { }
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@Bean
 | |
| 	fun userDetailsService(): MapReactiveUserDetailsService {
 | |
| 		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
 | |
| 		val rob = userBuilder.username("rob")
 | |
| 			.password("rob")
 | |
| 			.roles("USER")
 | |
| 			.build()
 | |
| 		val admin = userBuilder.username("admin")
 | |
| 			.password("admin")
 | |
| 			.roles("USER", "ADMIN")
 | |
| 			.build()
 | |
| 		return MapReactiveUserDetailsService(rob, admin)
 | |
| 	}
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| You can find a complete sample in {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method].
 |