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].
|