123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- [[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)));
- }
- ----
- ======
- 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.
- === 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 GrantedAuthorityDefaults
- [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]).
- ====
- [[use-programmatic-authorization]]
- == Authorizing Methods Programmatically
- As you've already seen, there are several ways that you can specify non-trivial authorization rules using xref:servlet/authorization/method-security.adoc#authorization-expressions[Method Security SpEL expressions].
- There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based.
- This gives use access the entire Java language for increased testability and flow control.
- === Using a Custom Bean in SpEL
- The first way to authorize a method programmatically is a two-step process.
- First, declare a bean that has a method that takes a `MethodSecurityExpressionOperations` instance like the following:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Component("authz")
- public class AuthorizationLogic {
- public decide(MethodSecurityExpressionOperations operations): Mono<Boolean> {
- // ... authorization logic
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Component("authz")
- open class AuthorizationLogic {
- fun decide(val operations: MethodSecurityExpressionOperations): Mono<Boolean> {
- // ... authorization logic
- }
- }
- ----
- ======
- Then, reference that bean in your annotations in the following way:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Controller
- public class MyController {
- @PreAuthorize("@authz.decide(#root)")
- @GetMapping("/endpoint")
- public Mono<String> endpoint() {
- // ...
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Controller
- open class MyController {
- @PreAuthorize("@authz.decide(#root)")
- @GetMapping("/endpoint")
- fun endpoint(): Mono<String> {
- // ...
- }
- }
- ----
- ======
- Spring Security will invoke the given method on that bean for each method invocation.
- What's nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness.
- It also has access to the full Java language.
- [TIP]
- In addition to returning a `Mono<Boolean>`, you can also return `Mono.empty()` to indicate that the code abstains from making a decision.
- If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Component("authz")
- public class AuthorizationLogic {
- public Mono<AuthorizationDecision> decide(MethodSecurityExpressionOperations operations) {
- // ... authorization logic
- return Mono.just(new MyAuthorizationDecision(false, details));
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Component("authz")
- open class AuthorizationLogic {
- fun decide(val operations: MethodSecurityExpressionOperations): Mono<AuthorizationDecision> {
- // ... authorization logic
- return Mono.just(MyAuthorizationDecision(false, details))
- }
- }
- ----
- ======
- Or throw a custom `AuthorizationDeniedException` instance.
- Note, though, that returning an object is preferred as this doesn't incur the expense of generating a stacktrace.
- Then, you can access the custom details when you xref:servlet/authorization/method-security.adoc#fallback-values-authorization-denied[customize how the authorization result is handled].
- [[jc-reactive-method-security-custom-authorization-manager]]
- [[custom-authorization-managers]]
- === Using a Custom Authorization Manager
- The second way to authorize a method programmatically is to create a custom xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[`AuthorizationManager`].
- First, declare an authorization manager instance, perhaps like this one:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Component
- public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
- @Override
- public Mono<AuthorizationDecision> check(Supplier<Authentication> authentication, MethodInvocation invocation) {
- // ... authorization logic
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Component
- class MyPreAuthorizeAuthorizationManager : ReactiveAuthorizationManager<MethodInvocation> {
- override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): Mono<AuthorizationDecision> {
- // ... authorization logic
- }
- }
- ----
- ======
- Then, publish the method interceptor with a pointcut that corresponds to when you want that `ReactiveAuthorizationManager` to run.
- For example, you could replace how `@PreAuthorize` and `@PostAuthorize` work like so:
- .Only @PreAuthorize and @PostAuthorize Configuration
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Configuration
- @EnableMethodSecurity(prePostEnabled = false)
- class MethodSecurityConfig {
- @Bean
- @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
- Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
- return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager);
- }
- @Bean
- @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
- Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
- return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager);
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Configuration
- @EnableMethodSecurity(prePostEnabled = false)
- class MethodSecurityConfig {
- @Bean
- @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
- fun preAuthorize(val manager: MyPreAuthorizeAuthorizationManager) : Advisor {
- return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager)
- }
- @Bean
- @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
- fun postAuthorize(val manager: MyPostAuthorizeAuthorizationManager) : Advisor {
- return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager)
- }
- }
- ----
- ======
- [TIP]
- ====
- You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
- ====
- [[customizing-expression-handling]]
- === Customizing Expression Handling
- Or, third, you can customize how each SpEL expression is handled.
- To do that, you can expose a custom `MethodSecurityExpressionHandler`, like so:
- .Custom MethodSecurityExpressionHandler
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
- DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
- handler.setRoleHierarchy(roleHierarchy);
- return handler;
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- companion object {
- @Bean
- fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
- val handler = DefaultMethodSecurityExpressionHandler()
- handler.setRoleHierarchy(roleHierarchy)
- return handler
- }
- }
- ----
- ======
- [TIP]
- ====
- We expose `MethodSecurityExpressionHandler` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
- ====
- You can also subclass xref:servlet/authorization/method-security.adoc#subclass-defaultmethodsecurityexpressionhandler[`DefaultMessageSecurityExpressionHandler`] to add your own custom authorization expressions beyond the defaults.
- == 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((authorize) -> authorize
- .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].
|