From 070dce1baf2f6ea801d4f2f28c16defb47442a2b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 25 Aug 2022 11:34:04 -0600 Subject: [PATCH] Document ReactiveMethodSecurity improvements Issue gh-9401 --- .../pages/reactive/authorization/method.adoc | 278 +++++++++++++++++- 1 file changed, 276 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/authorization/method.adoc b/docs/modules/ROOT/pages/reactive/authorization/method.adoc index 8c64598dfa..1f9b9a844d 100644 --- a/docs/modules/ROOT/pages/reactive/authorization/method.adoc +++ b/docs/modules/ROOT/pages/reactive/authorization/method.adoc @@ -10,11 +10,285 @@ For this to work the return type of the method must be a `org.reactivestreams.Pu 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. Note that we are waiting on https://github.com/spring-projects/spring-framework/issues/22462[additional coroutine support from the Spring Framework] before adding coroutine support. +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 <>. +==== + +For example, the following would enable Spring Security's `@PreAuthorize` annotation: + +.Method Security Configuration +==== +.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 +==== +.Java +[source,java,role="primary"] +---- +public interface BankService { + @PreAuthorize("hasRole('USER')") + Mono readAccount(Long id); + + @PreAuthorize("hasRole('USER')") + Flux findAccounts(); + + @PreAuthorize("@func.apply(#account)") + Mono 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` to indicate the authorization decision. +A bean like that might look something like this: + +.Method Security Reactive Boolean Expression +==== +.Java +[source,java,role="primary"] +---- +@Bean +public Function> 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 +==== +.Java +[source,java,role="primary"] +---- +@Bean +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 +==== + +[[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 +==== +.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 +==== +.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 +==== +.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 +==== + +.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 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 +==== +.Java +[source,java,role="primary"] +---- +public interface BankService { + + @PreAuthorize("hasRole('USER')") + @PostAuthorize("returnObject.owner == authentication.name") + Mono 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 +==== +.Java +[source,java,role="primary"] +---- +@EnableReactiveMethodSecurity(useAuthorizationManager=true) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public Advisor customAuthorize(ReactiveAuthorizationManager 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 + [WARNING] ==== -Method Security also supports Kotlin coroutines, though only to a limited degree. +`@EnableReactiveMethodSecurity` also supports Kotlin coroutines, though only to a limited degree. When intercepting coroutines, only the first interceptor participates. -If any other interceptors are present and come after Spring Security's method security interceptor, they will be skipped. +If any other interceptors are present and come after Spring Security's method security interceptor, https://github.com/spring-projects/spring-framework/issues/22462[they will be skipped]. ==== ====