This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 6.3.3! |
This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 6.3.3! |
Spring Security supports method security by using Reactor’s Context, which is set up by ReactiveSecurityContextHolder
.
The following example shows how to retrieve the currently logged in user’s message:
For this example to work, the return type of the method must be a |
For this example to work, the return type of the method must be a |
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)
:
-
Uses the simplified
AuthorizationManager
API instead of metadata sources, config attributes, decision managers, and voters. This simplifies reuse and customization. -
Supports reactive return types including Kotlin coroutines.
-
Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
-
Checks for conflicting annotations to ensure an unambiguous security configuration
-
Complies with JSR-250
For earlier versions, please read about similar support with @EnableReactiveMethodSecurity. |
For example, the following would enable Spring Security’s @PreAuthorize
annotation:
-
Java
@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:
-
Java
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:
-
Java
@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.
Before-method authorization is performed before the method is invoked.
If that authorization denies access, the method is not invoked, and an |
To recreate what adding @EnableReactiveMethodSecurity(useAuthorizationManager=true)
does by default, you would publish the following configuration:
-
Java
@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.
-
Java
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
We expose |
For earlier versions, please read about similar support with @EnableReactiveMethodSecurity. |
Before-method authorization is performed before the method is invoked.
If that authorization denies access, the method is not invoked, and an |
We expose |
Authorizing Methods Programmatically
As you’ve already seen, there are several ways that you can specify non-trivial authorization rules using 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:
-
Java
-
Kotlin
@Component("authz")
public class AuthorizationLogic {
public decide(MethodSecurityExpressionOperations operations): Mono<Boolean> {
// ... authorization logic
}
}
@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:
-
Java
-
Kotlin
@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public Mono<String> endpoint() {
// ...
}
}
@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.
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:
-
Java
-
Kotlin
@Component("authz")
public class AuthorizationLogic {
public Mono<AuthorizationDecision> decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
return Mono.just(new MyAuthorizationDecision(false, details));
}
}
@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 customize how the authorization result is handled.
Using a Custom Authorization Manager
The second way to authorize a method programmatically is to create a custom AuthorizationManager
.
First, declare an authorization manager instance, perhaps like this one:
-
Java
-
Kotlin
@Component
public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
@Override
public Mono<AuthorizationDecision> check(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
}
@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:
-
Java
-
Kotlin
@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);
}
}
@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)
}
}
You can place your interceptor in between Spring Security method interceptors using the order constants specified in |
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:
-
Java
-
Kotlin
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
companion object {
@Bean
fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
val handler = DefaultMethodSecurityExpressionHandler()
handler.setRoleHierarchy(roleHierarchy)
return handler
}
}
We expose |
You can also subclass DefaultMessageSecurityExpressionHandler
to add your own custom authorization expressions beyond the defaults.
In addition to returning a Mono<Boolean> , you can also return Mono.empty() to indicate that the code abstains from making a decision.
|
You can place your interceptor in between Spring Security method interceptors using the order constants specified in |
We expose |
EnableReactiveMethodSecurity
-
Java
-
Kotlin
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();
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:
-
Java
-
Kotlin
Mono<String> findMessageByUsername(String username) {
return Mono.just("Hi " + username);
}
fun findMessageByUsername(username: String): Mono<String> {
return Mono.just("Hi $username")
}
The following minimal method security configures method security in reactive applications:
-
Java
-
Kotlin
@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);
}
}
@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:
-
Java
-
Kotlin
@Component
public class HelloWorldMessageService {
@PreAuthorize("hasRole('ADMIN')")
public Mono<String> findMessage() {
return Mono.just("Hello World!");
}
}
@Component
class HelloWorldMessageService {
@PreAuthorize("hasRole('ADMIN')")
fun findMessage(): Mono<String> {
return Mono.just("Hello World!")
}
}
Alternatively, the following class uses Kotlin coroutines:
-
Kotlin
@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 WebFlux Security, the Reactor Context is automatically established by Spring Security according to the authenticated user:
-
Java
-
Kotlin
@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);
}
}
@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 hellowebflux-method.