In addition to modeling authorization at the request level, Spring Security also supports modeling at the method level.
You can activate it in your application by annotating any @Configuration
class with @EnableMethodSecurity
or adding <method-security>
to any XML configuration file, like so:
- Java
-
@EnableMethodSecurity
- Kotlin
-
@EnableMethodSecurity
- Xml
-
<sec:method-security/>
Then, you are immediately able to annotate any Spring-managed class or method with @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
to authorize method invocations, including the input parameters and return values.
Note
|
{spring-boot-reference-url}reference/using/build-systems.html#using.build-systems.starters[Spring Boot Starter Security] does not activate method-level authorization by default. |
Method Security supports many other use cases as well including AspectJ support, custom annotations, and several configuration points. Consider learning about the following use cases:
-
Understanding how method security works and reasons to use it
-
Authorizing methods with
@PreAuthorize
and@PostAuthorize
-
Filtering methods with
@PreFilter
and@PostFilter
-
Authorizing methods with JSR-250 annotations
-
Authorizing methods with AspectJ expressions
-
Integrating with AspectJ byte-code weaving
-
Coordinating with @Transactional and other AOP-based annotations
-
Customizing SpEL expression handling
-
Integrating with custom authorization systems
Spring Security’s method authorization support is handy for:
-
Extracting fine-grained authorization logic; for example, when the method parameters and return values contribute to the authorization decision.
-
Enforcing security at the service layer
-
Stylistically favoring annotation-based over
HttpSecurity
-based configuration
And since Method Security is built using {spring-framework-reference-url}core.html#aop-api[Spring AOP], you have access to all its expressive power to override Spring Security’s defaults as needed.
As already mentioned, you begin by adding @EnableMethodSecurity
to a @Configuration
class or <sec:method-security/>
in a Spring XML configuration file.
Note
|
This annotation and XML element supercede
If you are using |
Method authorization is a combination of before- and after-method authorization. Consider a service bean that is annotated in the following way:
- Java
-
@Service public class MyCustomerService { @PreAuthorize("hasAuthority('permission:read')") @PostAuthorize("returnObject.owner == authentication.name") public Customer readCustomer(String id) { ... } }
- Kotlin
-
@Service open class MyCustomerService { @PreAuthorize("hasAuthority('permission:read')") @PostAuthorize("returnObject.owner == authentication.name") fun readCustomer(val id: String): Customer { ... } }
A given invocation to MyCustomerService#readCustomer
may look something like this when Method Security is activated:
-
Spring AOP invokes its proxy method for
readCustomer
. Among the proxy’s other advisors, it invokes an javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[] that matches the@PreAuthorize
pointcut -
The interceptor invokes javadoc:org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager[
PreAuthorizeAuthorizationManager#check
] -
The authorization manager uses a
MethodSecurityExpressionHandler
to parse the annotation’s SpEL expression and constructs a correspondingEvaluationContext
from aMethodSecurityExpressionRoot
containing aSupplier<Authentication>
andMethodInvocation
. -
The interceptor uses this context to evaluate the expression; specifically, it reads the
Authentication
from theSupplier
and checks whether it haspermission:read
in its collection of authorities -
If the evaluation passes, then Spring AOP proceeds to invoke the method.
-
If not, the interceptor publishes an
AuthorizationDeniedEvent
and throws an javadoc:org.springframework.security.access.AccessDeniedException[] which theExceptionTranslationFilter
catches and returns a 403 status code to the response -
After the method returns, Spring AOP invokes an javadoc:org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor[] that matches the
@PostAuthorize
pointcut, operating the same as above, but with javadoc:org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager[] -
If the evaluation passes (in this case, the return value belongs to the logged-in user), processing continues normally
-
If not, the interceptor publishes an
AuthorizationDeniedEvent
and throws an javadoc:org.springframework.security.access.AccessDeniedException[], which theExceptionTranslationFilter
catches and returns a 403 status code to the response
Note
|
If the method is not being called in the context of an HTTP request, you will likely need to handle the AccessDeniedException yourself
|
As demonstrated above, if a method invocation involves multiple Method Security annotations, each of those is processed one at a time. This means that they can collectively be thought of as being "anded" together. In other words, for an invocation to be authorized, all annotation inspections need to pass authorization.
That said, it is not supported to repeat the same annotation on the same method.
For example, you cannot place @PreAuthorize
twice on the same method.
Instead, use SpEL’s boolean support or its support for delegating to a separate bean.
Each annotation has its own pointcut instance that looks for that annotation or its meta-annotation counterparts across the entire object hierarchy, starting at the method and its enclosing class.
Each annotation has its own dedicated method interceptor.
The reason for this is to make things more composable.
For example, if needed, you can disable the Spring Security defaults and publish only the @PostAuthorize
method interceptor.
The method interceptors are as follows:
-
For
@PreAuthorize
, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[AuthorizationManagerBeforeMethodInterceptor#preAuthorize
], which in turn uses javadoc:org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager[] -
For
@PostAuthorize
, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor[AuthorizationManagerAfterMethodInterceptor#postAuthorize
], which in turn uses javadoc:org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager[] -
For
@PreFilter
, Spring Security uses javadoc:org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor[] -
For
@PostFilter
, Spring Security uses javadoc:org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor[] -
For
@Secured
, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[AuthorizationManagerBeforeMethodInterceptor#secured
], which in turn uses javadoc:org.springframework.security.authorization.method.SecuredAuthorizationManager[] -
For JSR-250 annotations, Spring Security uses javadoc:org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor[
AuthorizationManagerBeforeMethodInterceptor#jsr250
], which in turn uses javadoc:org.springframework.security.authorization.method.Jsr250AuthorizationManager[]
Generally speaking, you can consider the following listing as representative of what interceptors Spring Security publishes when you add @EnableMethodSecurity
:
- Java
-
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static Advisor preAuthorizeMethodInterceptor() { return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static Advisor postAuthorizeMethodInterceptor() { return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static Advisor preFilterMethodInterceptor() { return AuthorizationManagerBeforeMethodInterceptor.preFilter(); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static Advisor postFilterMethodInterceptor() { return AuthorizationManagerAfterMethodInterceptor.postFilter(); }
Quite often it can be tempting to introduce a complicated SpEL expression like the following:
- Java
-
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
However, you could instead grant permission:read
to those with ROLE_ADMIN
.
One way to do this is with a RoleHierarchy
like so:
- Java
-
@Bean static RoleHierarchy roleHierarchy() { return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read"); }
- Kotlin
-
companion object { @Bean fun roleHierarchy(): RoleHierarchy { return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read") } }
- Xml
-
<bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy"> <constructor-arg value="ROLE_ADMIN > permission:read"/> </bean>
and then set that in a MethodSecurityExpressionHandler
instance.
This then allows you to have a simpler @PreAuthorize
expression like this one:
- Java
-
@PreAuthorize("hasAuthority('permission:read')")
- Kotlin
-
@PreAuthorize("hasAuthority('permission:read')")
Or, where possible, adapt application-specific authorization logic into granted authorities at login time.
When should you favor method-level authorization over request-level authorization? Some of it comes down to taste; however, consider the following strengths list of each to help you decide.
request-level |
method-level |
|
authorization type |
coarse-grained |
fine-grained |
configuration location |
declared in a config class |
local to method declaration |
configuration style |
DSL |
Annotations |
authorization definitions |
programmatic |
SpEL |
The main tradeoff seems to be where you want your authorization rules to live.
Note
|
It’s important to remember that when you use annotation-based Method Security, then unannotated methods are not secured.
To protect against this, declare a catch-all authorization rule in your HttpSecurity instance.
|
The primary way Spring Security enables method-level authorization support is through annotations that you can add to methods, classes, and interfaces.
When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PreAuthorize[format=annotation] annotation like so:
- Java
-
@Component public class BankService { @PreAuthorize("hasRole('ADMIN')") public Account readAccount(Long id) { // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority } }
- Kotlin
-
@Component open class BankService { @PreAuthorize("hasRole('ADMIN')") fun readAccount(val id: Long): Account { // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority } }
This is meant to indicate that the method can only be invoked if the provided expression hasRole('ADMIN')
passes.
You can then test the class to confirm it is enforcing the authorization rule like so:
- Java
-
@Autowired BankService bankService; @WithMockUser(roles="ADMIN") @Test void readAccountWithAdminRoleThenInvokes() { Account account = this.bankService.readAccount("12345678"); // ... assertions } @WithMockUser(roles="WRONG") @Test void readAccountWithWrongRoleThenAccessDenied() { assertThatExceptionOfType(AccessDeniedException.class).isThrownBy( () -> this.bankService.readAccount("12345678")); }
- Kotlin
-
@WithMockUser(roles="ADMIN") @Test fun readAccountWithAdminRoleThenInvokes() { val account: Account = this.bankService.readAccount("12345678") // ... assertions } @WithMockUser(roles="WRONG") @Test fun readAccountWithWrongRoleThenAccessDenied() { assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { this.bankService.readAccount("12345678") } }
Tip
|
@PreAuthorize also can be a meta-annotation, be defined at the class or interface level, and use SpEL Authorization Expressions.
|
While @PreAuthorize
is quite helpful for declaring needed authorities, it can also be used to evaluate more complex expressions that involve the method parameters.
When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PostAuthorize[format=annotation] annotation like so:
- Java
-
@Component public class BankService { @PostAuthorize("returnObject.owner == authentication.name") public Account readAccount(Long id) { // ... is only returned if the `Account` belongs to the logged in user } }
- Kotlin
-
@Component open class BankService { @PostAuthorize("returnObject.owner == authentication.name") fun readAccount(val id: Long): Account { // ... is only returned if the `Account` belongs to the logged in user } }
This is meant to indicate that the method can only return the value if the provided expression returnObject.owner == authentication.name
passes.
returnObject
represents the Account
object to be returned.
You can then test the class to confirm it is enforcing the authorization rule:
- Java
-
@Autowired BankService bankService; @WithMockUser(username="owner") @Test void readAccountWhenOwnedThenReturns() { Account account = this.bankService.readAccount("12345678"); // ... assertions } @WithMockUser(username="wrong") @Test void readAccountWhenNotOwnedThenAccessDenied() { assertThatExceptionOfType(AccessDeniedException.class).isThrownBy( () -> this.bankService.readAccount("12345678")); }
- Kotlin
-
@WithMockUser(username="owner") @Test fun readAccountWhenOwnedThenReturns() { val account: Account = this.bankService.readAccount("12345678") // ... assertions } @WithMockUser(username="wrong") @Test fun readAccountWhenNotOwnedThenAccessDenied() { assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { this.bankService.readAccount("12345678") } }
Tip
|
@PostAuthorize also can be a meta-annotation, be defined at the class or interface level, and use SpEL Authorization Expressions.
|
@PostAuthorize
is particularly helpful when defending against Insecure Direct Object Reference.
In fact, it can be defined as a meta-annotation like so:
- Java
-
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PostAuthorize("returnObject.owner == authentication.name") public @interface RequireOwnership {}
- Kotlin
-
@Target(ElementType.METHOD, ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @PostAuthorize("returnObject.owner == authentication.name") annotation class RequireOwnership
Allowing you to instead annotate the service in the following way:
- Java
-
@Component public class BankService { @RequireOwnership public Account readAccount(Long id) { // ... is only returned if the `Account` belongs to the logged in user } }
- Kotlin
-
@Component open class BankService { @RequireOwnership fun readAccount(val id: Long): Account { // ... is only returned if the `Account` belongs to the logged in user } }
The result is that the above method will only return the Account
if its owner
attribute matches the logged-in user’s name
.
If not, Spring Security will throw an AccessDeniedException
and return a 403 status code.
When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PreFilter[format=annotation] annotation like so:
- Java
-
@Component public class BankService { @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Account... accounts) { // ... `accounts` will only contain the accounts owned by the logged-in user return updated; } }
- Kotlin
-
@Component open class BankService { @PreFilter("filterObject.owner == authentication.name") fun updateAccounts(vararg accounts: Account): Collection<Account> { // ... `accounts` will only contain the accounts owned by the logged-in user return updated } }
This is meant to filter out any values from accounts
where the expression filterObject.owner == authentication.name
fails.
filterObject
represents each account
in accounts
and is used to test each account
.
You can then test the class in the following way to confirm it is enforcing the authorization rule:
- Java
-
@Autowired BankService bankService; @WithMockUser(username="owner") @Test void updateAccountsWhenOwnedThenReturns() { Account ownedBy = ... Account notOwnedBy = ... Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy); assertThat(updated).containsOnly(ownedBy); }
- Kotlin
-
@Autowired lateinit var bankService: BankService @WithMockUser(username="owner") @Test fun updateAccountsWhenOwnedThenReturns() { val ownedBy: Account = ... val notOwnedBy: Account = ... val updated: Collection<Account> = bankService.updateAccounts(ownedBy, notOwnedBy) assertThat(updated).containsOnly(ownedBy) }
Tip
|
@PreFilter also can be a meta-annotation, be defined at the class or interface level, and use SpEL Authorization Expressions.
|
@PreFilter
supports arrays, collections, maps, and streams (so long as the stream is still open).
For example, the above updateAccounts
declaration will function the same way as the following other four:
- Java
-
@PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Account[] accounts) @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Collection<Account> accounts) @PreFilter("filterObject.value.owner == authentication.name") public Collection<Account> updateAccounts(Map<String, Account> accounts) @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Stream<Account> accounts)
- Kotlin
-
@PreFilter("filterObject.owner == authentication.name") fun updateAccounts(accounts: Array<Account>): Collection<Account> @PreFilter("filterObject.owner == authentication.name") fun updateAccounts(accounts: Collection<Account>): Collection<Account> @PreFilter("filterObject.value.owner == authentication.name") fun updateAccounts(accounts: Map<String, Account>): Collection<Account> @PreFilter("filterObject.owner == authentication.name") fun updateAccounts(accounts: Stream<Account>): Collection<Account>
The result is that the above method will only have the Account
instances where their owner
attribute matches the logged-in user’s name
.
When Method Security is active, you can annotate a method with the javadoc:org.springframework.security.access.prepost.PostFilter[format=annotation] annotation like so:
- Java
-
@Component public class BankService { @PostFilter("filterObject.owner == authentication.name") public Collection<Account> readAccounts(String... ids) { // ... the return value will be filtered to only contain the accounts owned by the logged-in user return accounts; } }
- Kotlin
-
@Component open class BankService { @PostFilter("filterObject.owner == authentication.name") fun readAccounts(vararg ids: String): Collection<Account> { // ... the return value will be filtered to only contain the accounts owned by the logged-in user return accounts } }
This is meant to filter out any values from the return value where the expression filterObject.owner == authentication.name
fails.
filterObject
represents each account
in accounts
and is used to test each account
.
You can then test the class like so to confirm it is enforcing the authorization rule:
- Java
-
@Autowired BankService bankService; @WithMockUser(username="owner") @Test void readAccountsWhenOwnedThenReturns() { Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner"); assertThat(accounts).hasSize(1); assertThat(accounts.get(0).getOwner()).isEqualTo("owner"); }
- Kotlin
-
@Autowired lateinit var bankService: BankService @WithMockUser(username="owner") @Test fun readAccountsWhenOwnedThenReturns() { val accounts: Collection<Account> = bankService.updateAccounts("owner", "not-owner") assertThat(accounts).hasSize(1) assertThat(accounts[0].owner).isEqualTo("owner") }
Tip
|
@PostFilter also can be a meta-annotation, be defined at the class or interface level, and use SpEL Authorization Expressions.
|
@PostFilter
supports arrays, collections, maps, and streams (so long as the stream is still open).
For example, the above readAccounts
declaration will function the same way as the following other three:
- Java
-
@PostFilter("filterObject.owner == authentication.name") public Collection<Account> readAccounts(String... ids) @PostFilter("filterObject.owner == authentication.name") public Account[] readAccounts(String... ids) @PostFilter("filterObject.value.owner == authentication.name") public Map<String, Account> readAccounts(String... ids) @PostFilter("filterObject.owner == authentication.name") public Stream<Account> readAccounts(String... ids)
- Kotlin
-
@PostFilter("filterObject.owner == authentication.name") fun readAccounts(vararg ids: String): Collection<Account> @PostFilter("filterObject.owner == authentication.name") fun readAccounts(vararg ids: String): Array<Account> @PostFilter("filterObject.owner == authentication.name") fun readAccounts(vararg ids: String): Map<String, Account> @PostFilter("filterObject.owner == authentication.name") fun readAccounts(vararg ids: String): Stream<Account>
The result is that the above method will return the Account
instances where their owner
attribute matches the logged-in user’s name
.
Note
|
In-memory filtering can obviously be expensive, and so be considerate of whether it is better to filter the data in the data layer instead. |
javadoc:org.springframework.security.access.annotation.Secured[format=annotation] is a legacy option for authorizing invocations.
@PreAuthorize
supersedes it and is recommended instead.
To use the @Secured
annotation, you should first change your Method Security declaration to enable it like so:
- Java
-
@EnableMethodSecurity(securedEnabled = true)
- Kotlin
-
@EnableMethodSecurity(securedEnabled = true)
- Xml
-
<sec:method-security secured-enabled="true"/>
This will cause Spring Security to publish the corresponding method interceptor that authorizes methods, classes, and interfaces annotated with @Secured
.
In case you would like to use JSR-250 annotations, Spring Security also supports that.
@PreAuthorize
has more expressive power and is thus recommended.
To use the JSR-250 annotations, you should first change your Method Security declaration to enable them like so:
- Java
-
@EnableMethodSecurity(jsr250Enabled = true)
- Kotlin
-
@EnableMethodSecurity(jsr250Enabled = true)
- Xml
-
<sec:method-security jsr250-enabled="true"/>
This will cause Spring Security to publish the corresponding method interceptor that authorizes methods, classes, and interfaces annotated with @RolesAllowed
, @PermitAll
, and @DenyAll
.
It’s also supported to have Method Security annotations at the class and interface level.
If it is at the class level like so:
- Java
-
@Controller @PreAuthorize("hasAuthority('ROLE_USER')") public class MyController { @GetMapping("/endpoint") public String endpoint() { ... } }
- Kotlin
-
@Controller @PreAuthorize("hasAuthority('ROLE_USER')") open class MyController { @GetMapping("/endpoint") fun endpoint(): String { ... } }
then all methods inherit the class-level behavior.
Or, if it’s declared like the following at both the class and method level:
- Java
-
@Controller @PreAuthorize("hasAuthority('ROLE_USER')") public class MyController { @GetMapping("/endpoint") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public String endpoint() { ... } }
- Kotlin
-
@Controller @PreAuthorize("hasAuthority('ROLE_USER')") open class MyController { @GetMapping("/endpoint") @PreAuthorize("hasAuthority('ROLE_ADMIN')") fun endpoint(): String { ... } }
then methods declaring the annotation override the class-level annotation.
The same is true for interfaces, with the exception that if a class inherits the annotation from two different interfaces, then startup will fail. This is because Spring Security has no way to tell which one you want to use.
In cases like this, you can resolve the ambiguity by adding the annotation to the concrete method.
Method Security supports meta annotations. This means that you can take any annotation and improve readability based on your application-specific use cases.
For example, you can simplify @PreAuthorize("hasRole('ADMIN')")
to @IsAdmin
like so:
- Java
-
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") public @interface IsAdmin {}
- Kotlin
-
@Target(ElementType.METHOD, ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") annotation class IsAdmin
And the result is that on your secured methods you can now do the following instead:
- Java
-
@Component public class BankService { @IsAdmin public Account readAccount(Long id) { // ... is only returned if the `Account` belongs to the logged in user } }
- Kotlin
-
@Component open class BankService { @IsAdmin fun readAccount(val id: Long): Account { // ... is only returned if the `Account` belongs to the logged in user } }
This results in more readable method definitions.
You can also opt into using meta-annotation templates, which allow for much more powerful annotation definitions.
First, publish the following bean:
- Java
-
@Bean static AnnotationTemplateExpressionDefaults templateExpressionDefaults() { return new AnnotationTemplateExpressionDefaults(); }
- Kotlin
-
companion object { @Bean fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults { return AnnotationTemplateExpressionDefaults() } }
Now instead of @IsAdmin
, you can create something more powerful like @HasRole
like so:
- Java
-
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('{value}')") public @interface HasRole { String value(); }
- Kotlin
-
@Target(ElementType.METHOD, ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('{value}')") annotation class HasRole(val value: String)
And the result is that on your secured methods you can now do the following instead:
- Java
-
@Component public class BankService { @HasRole("ADMIN") public Account readAccount(Long id) { // ... is only returned if the `Account` belongs to the logged in user } }
- Kotlin
-
@Component open class BankService { @HasRole("ADMIN") fun readAccount(val id: Long): Account { // ... is only returned if the `Account` belongs to the logged in user } }
Note that this works with method variables and all annotation types, too, though you will want to be careful to correctly take care of quotation marks so the resulting SpEL expression is correct.
For example, consider the following @HasAnyRole
annotation:
- Java
-
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasAnyRole({roles})") public @interface HasAnyRole { String[] roles(); }
- Kotlin
-
@Target(ElementType.METHOD, ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasAnyRole({roles})") annotation class HasAnyRole(val roles: Array<String>)
In that case, you’ll notice that you should not use the quotation marks in the expression, but instead in the parameter value like so:
- Java
-
@Component public class BankService { @HasAnyRole(roles = { "'USER'", "'ADMIN'" }) public Account readAccount(Long id) { // ... is only returned if the `Account` belongs to the logged in user } }
- Kotlin
-
@Component open class BankService { @HasAnyRole(roles = arrayOf("'USER'", "'ADMIN'")) fun readAccount(val id: Long): Account { // ... is only returned if the `Account` belongs to the logged in user } }
so that, once replaced, the expression becomes @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
.
You can turn off @EnableMethodSecurity
's pre-configuration and replace it with you own.
You may choose to do this if you want to customize the AuthorizationManager
or Pointcut
.
Or you may simply want to only enable a specific annotation, like @PostAuthorize
.
You can do this in the following way:
- Java
-
@Configuration @EnableMethodSecurity(prePostEnabled = false) class MethodSecurityConfig { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor postAuthorize() { return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); } }
- Kotlin
-
@Configuration @EnableMethodSecurity(prePostEnabled = false) class MethodSecurityConfig { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) fun postAuthorize() : Advisor { return AuthorizationManagerAfterMethodInterceptor.postAuthorize() } }
- Xml
-
<sec:method-security pre-post-enabled="false"/> <aop:config/> <bean id="postAuthorize" class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor" factory-method="postAuthorize"/>
The above snippet achieves this by first disabling Method Security’s pre-configurations and then publishing the @PostAuthorize
interceptor itself.
While using Spring Security’s annotation-based support is preferred for method security, you can also use XML to declare bean authorization rules.
If you need to declare it in your XML configuration instead, you can use <intercept-methods>
like so:
- Xml
-
<bean class="org.mycompany.MyController"> <intercept-methods> <protect method="get*" access="hasAuthority('read')"/> <protect method="*" access="hasAuthority('write')"/> </intercept-methods> </bean>
Note
|
This only supports matching method by prefix or by name. If your needs are more complex than that, use annotation support instead. |
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.
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
-
@Component("authz") public class AuthorizationLogic { public boolean decide(MethodSecurityExpressionOperations operations) { // ... authorization logic } }
- Kotlin
-
@Component("authz") open class AuthorizationLogic { fun decide(val operations: MethodSecurityExpressionOperations): boolean { // ... authorization logic } }
Then, reference that bean in your annotations in the following way:
- Java
-
@Controller public class MyController { @PreAuthorize("@authz.decide(#root)") @GetMapping("/endpoint") public String endpoint() { // ... } }
- Kotlin
-
@Controller open class MyController { @PreAuthorize("@authz.decide(#root)") @GetMapping("/endpoint") fun String endpoint() { // ... } }
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 Boolean , you can also return null 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
-
@Component("authz") public class AuthorizationLogic { public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) { // ... authorization logic return new MyAuthorizationDecision(false, details); } }
- Kotlin
-
@Component("authz") open class AuthorizationLogic { fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision { // ... authorization logic return 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.
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
-
@Component public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> { @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) { // ... authorization logic } @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) { // ... authorization logic } }
- Kotlin
-
@Component class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> { override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision { // ... authorization logic } override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision { // ... authorization logic } }
Then, publish the method interceptor with a pointcut that corresponds to when you want that AuthorizationManager
to run.
For example, you could replace how @PreAuthorize
and @PostAuthorize
work like so:
- Java
-
@Configuration @EnableMethodSecurity(prePostEnabled = false) class MethodSecurityConfig { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor preAuthorize(MyAuthorizationManager manager) { return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor postAuthorize(MyAuthorizationManager manager) { return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager); } }
- Kotlin
-
@Configuration @EnableMethodSecurity(prePostEnabled = false) class MethodSecurityConfig { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) fun preAuthorize(val manager: MyAuthorizationManager) : Advisor { return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager) } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) fun postAuthorize(val manager: MyAuthorizationManager) : Advisor { return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager) } }
- Xml
-
<sec:method-security pre-post-enabled="false"/> <aop:config/> <bean id="preAuthorize" class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor" factory-method="preAuthorize"> <constructor-arg ref="myAuthorizationManager"/> </bean> <bean id="postAuthorize" class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor" factory-method="postAuthorize"> <constructor-arg ref="myAuthorizationManager"/> </bean>
Tip
|
You can place your interceptor in between Spring Security method interceptors using the order constants specified in |
Or, third, you can customize how each SpEL expression is handled. To do that, you can expose a custom javadoc:org.springframework.security.access.expression.method.MethodSecurityExpressionHandler[], like so:
- Java
-
@Bean static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); handler.setRoleHierarchy(roleHierarchy); return handler; }
- Kotlin
-
companion object { @Bean fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler { val handler = DefaultMethodSecurityExpressionHandler() handler.setRoleHierarchy(roleHierarchy) return handler } }
- Xml
-
<sec:method-security> <sec:expression-handler ref="myExpressionHandler"/> </sec:method-security> <bean id="myExpressionHandler" class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler"> <property name="roleHierarchy" ref="roleHierarchy"/> </bean>
Tip
|
We expose |
You can also subclass DefaultMessageSecurityExpressionHandler
to add your own custom authorization expressions beyond the defaults.
Spring Security will scan all beans in the application context for methods that use @PreAuthorize
or @PostAuthorize
.
When it finds one, it will resolve any beans used inside the security expression and register the appropriate runtime hints for that bean.
If it finds a method that uses @AuthorizeReturnObject
, it will recursively search inside the method’s return type for @PreAuthorize
and @PostAuthorize
annotations and register them accordingly.
For example, consider the following Spring Boot application:
- Java
-
@Service public class AccountService { (1) @PreAuthorize("@authz.decide()") (2) @AuthorizeReturnObject (3) public Account getAccountById(String accountId) { // ... } } public class Account { private final String accountNumber; // ... @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4) public String getAccountNumber() { return this.accountNumber; } @AuthorizeReturnObject (5) public User getUser() { return new User("John Doe"); } } public class User { private final String fullName; // ... @PostAuthorize("@myOtherAuthz.decide()") (6) public String getFullName() { return this.fullName; } }
- Kotlin
-
@Service class AccountService { (1) @PreAuthorize("@authz.decide()") (2) @AuthorizeReturnObject (3) fun getAccountById(accountId: String): Account { // ... } } class Account(private val accountNumber: String) { @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4) fun getAccountNumber(): String { return this.accountNumber } @AuthorizeReturnObject (5) fun getUser(): User { return User("John Doe") } } class User(private val fullName: String) { @PostAuthorize("@myOtherAuthz.decide()") (6) fun getFullName(): String { return this.fullName } }
-
Spring Security finds the
AccountService
bean -
Finding a method that uses
@PreAuthorize
, it will resolve any bean names used inside the expression,authz
in that case, and register runtime hints for the bean class -
Finding a method that uses
@AuthorizeReturnObject
, it will look into the method’s return type for any@PreAuthorize
or@PostAuthorize
-
Then, it finds a
@PreAuthorize
with another bean name:accountAuthz
; the runtime hints are registered for the bean class as well -
Finding another
@AuthorizeReturnObject
it will look again into the method’s return type -
Now, a
@PostAuthorize
is found with yet another bean name used:myOtherAuthz
; the runtime hints are registered for the bean class as well
There will be many times when Spring Security cannot determine the actual return type of the method ahead of time since it may be hidden in an erased generic type.
Consider the following service:
- Java
-
@Service public class AccountService { @AuthorizeReturnObject public List<Account> getAllAccounts() { // ... } }
- Kotlin
-
@Service class AccountService { @AuthorizeReturnObject fun getAllAccounts(): List<Account> { // ... } }
In this case, the generic type is erased and so it isn’t apparent to Spring Security ahead-of-time that Account
needs to be visited in order to check for @PreAuthorize
and @PostAuthorize
.
To address this, you can publish a javadoc:org.springframework.security.aot.hint.PrePostAuthorizeExpressionBeanHintsRegistrar[PrePostAuthorizeExpressionBeanHintsRegistrar
] like so:
- Java
-
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static SecurityHintsRegistrar registerTheseToo() { return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class); }
- Kotlin
-
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) fun registerTheseToo(): SecurityHintsRegistrar { return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java) }
Being built on Spring AOP, you can declare patterns that are not related to annotations, similar to request-level authorization. This has the potential advantage of centralizing method-level authorization rules.
For example, you can use publish your own Advisor
or use <protect-pointcut>
to match AOP expressions to authorization rules for your service layer like so:
- Java
-
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static Advisor protectServicePointcut() { AspectJExpressionPointcut pattern = new AspectJExpressionPointcut() pattern.setExpression("execution(* com.mycompany.*Service.*(..))") return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER")) }
- Kotlin
-
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole companion object { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) fun protectServicePointcut(): Advisor { val pattern = AspectJExpressionPointcut() pattern.setExpression("execution(* com.mycompany.*Service.*(..))") return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER")) } }
- Xml
-
<sec:method-security> <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/> </sec:method-security>
Performance can at times be enhanced by using AspectJ to weave Spring Security advice into the byte code of your beans.
After setting up AspectJ, you can quite simply state in the @EnableMethodSecurity
annotation or <method-security>
element that you are using AspectJ:
- Java
-
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
- Kotlin
-
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
- Xml
-
<sec:method-security mode="aspectj"/>
And the result will be that Spring Security will publish its advisors as AspectJ advice so that they can be woven in accordingly.
As already noted, there is a Spring AOP method interceptor for each annotation, and each of these has a location in the Spring AOP advisor chain.
Namely, the @PreFilter
method interceptor’s order is 100, @PreAuthorize
's is 200, and so on.
The reason this is important to note is that there are other AOP-based annotations like @EnableTransactionManagement
that have an order of Integer.MAX_VALUE
.
In other words, they are located at the end of the advisor chain by default.
At times, it can be valuable to have other advice execute before Spring Security.
For example, if you have a method annotated with @Transactional
and @PostAuthorize
, you might want the transaction to still be open when @PostAuthorize
runs so that an AccessDeniedException
will cause a rollback.
To get @EnableTransactionManagement
to open a transaction before method authorization advice runs, you can set @EnableTransactionManagement
's order like so:
- Java
-
@EnableTransactionManagement(order = 0)
- Kotlin
-
@EnableTransactionManagement(order = 0)
- Xml
-
<tx:annotation-driven ref="txManager" order="0"/>
Since the earliest method interceptor (@PreFilter
) is set to an order of 100, a setting of zero means that the transaction advice will run before all Spring Security advice.
You’ve already seen several examples using SpEL, so now let’s cover the API a bit more in depth.
Spring Security encapsulates all of its authorization fields and methods in a set of root objects.
The most generic root object is called SecurityExpressionRoot
and it forms the basis for MethodSecurityExpressionRoot
.
Spring Security supplies this root object to MethodSecurityEvaluationContext
when preparing to evaluate an authorization expression.
The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. What follows is a quick overview of the most common methods:
-
permitAll
- The method requires no authorization to be invoked; note that in this case, theAuthentication
is never retrieved from the session -
denyAll
- The method is not allowed under any circumstances; note that in this case, theAuthentication
is never retrieved from the session -
hasAuthority
- The method requires that theAuthentication
have aGrantedAuthority
that matches the given value -
hasRole
- A shortcut forhasAuthority
that prefixesROLE_
or whatever is configured as the default prefix -
hasAnyAuthority
- The method requires that theAuthentication
have aGrantedAuthority
that matches any of the given values -
hasAnyRole
- A shortcut forhasAnyAuthority
that prefixesROLE_
or whatever is configured as the default prefix -
hasPermission
- A hook into yourPermissionEvaluator
instance for doing object-level authorization
And here is a brief look at the most common fields:
-
authentication
- TheAuthentication
instance associated with this method invocation -
principal
- TheAuthentication#getPrincipal
associated with this method invocation
Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:
- Java
-
@Component public class MyService { @PreAuthorize("denyAll") (1) MyResource myDeprecatedMethod(...); @PreAuthorize("hasRole('ADMIN')") (2) MyResource writeResource(...) @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3) MyResource deleteResource(...) @PreAuthorize("principal.claims['aud'] == 'my-audience'") (4) MyResource readResource(...); @PreAuthorize("@authz.check(authentication, #root)") MyResource shareResource(...); }
- Kotlin
-
@Component open class MyService { @PreAuthorize("denyAll") (1) fun myDeprecatedMethod(...): MyResource @PreAuthorize("hasRole('ADMIN')") (2) fun writeResource(...): MyResource @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3) fun deleteResource(...): MyResource @PreAuthorize("principal.claims['aud'] == 'my-audience'") (4) fun readResource(...): MyResource @PreAuthorize("@authz.check(#root)") fun shareResource(...): MyResource }
- Xml
-
<sec:method-security> <protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> (1) <protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> (2) <protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> (3) <protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> (4) <protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> (5) </sec:method-security>
-
This method may not be invoked by anyone for any reason
-
This method may only be invoked by
Authentication
s granted theROLE_ADMIN
authority -
This method may only be invoked by
Authentication
s granted thedb
andROLE_ADMIN
authorities -
This method may only be invoked by
Princpal
s with anaud
claim equal to "my-audience" -
This method may only be invoked if the bean
authz
'scheck
method returnstrue
Tip
|
You can use a bean like |
Additionally, Spring Security provides a mechanism for discovering method parameters so they can also be accessed in the SpEL expression as well.
For a complete reference, Spring Security uses DefaultSecurityParameterNameDiscoverer
to discover the parameter names.
By default, the following options are tried for a method.
-
If Spring Security’s
@P
annotation is present on a single argument to the method, the value is used. The following example uses the@P
annotation:- Java
-
import org.springframework.security.access.method.P; ... @PreAuthorize("hasPermission(#c, 'write')") public void updateContact(@P("c") Contact contact);
- Kotlin
-
import org.springframework.security.access.method.P ... @PreAuthorize("hasPermission(#c, 'write')") fun doSomething(@P("c") contact: Contact?)
The intention of this expression is to require that the current
Authentication
havewrite
permission specifically for thisContact
instance.Behind the scenes, this is implemented by using
AnnotationParameterNameDiscoverer
, which you can customize to support the value attribute of any specified annotation. -
If Spring Data’s
@Param
annotation is present on at least one parameter for the method, the value is used. The following example uses the@Param
annotation:- Java
-
import org.springframework.data.repository.query.Param; ... @PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name);
- Kotlin
-
import org.springframework.data.repository.query.Param ... @PreAuthorize("#n == authentication.name") fun findContactByName(@Param("n") name: String?): Contact?
The intention of this expression is to require that
name
be equal toAuthentication#getName
for the invocation to be authorized.Behind the scenes, this is implemented by using
AnnotationParameterNameDiscoverer
, which you can customize to support the value attribute of any specified annotation. -
If you compile your code with the
-parameters
argument, the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces. -
Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols. This does not work for interfaces, since they do not have debug information about the parameter names. For interfaces, either annotations or the
-parameters
approach must be used.
Spring Security also supports wrapping any object that is annotated its method security annotations.
The simplest way to achieve this is to mark any method that returns the object you wish to authorize with the @AuthorizeReturnObject
annotation.
For example, consider the following User
class:
- Java
-
public class User { private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } public String getName() { return this.name; } @PreAuthorize("hasAuthority('user:read')") public String getEmail() { return this.email; } }
- Kotlin
-
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)
Given an interface like this one:
- Java
-
public class UserRepository { @AuthorizeReturnObject Optional<User> findByName(String name) { // ... } }
- Kotlin
-
class UserRepository { @AuthorizeReturnObject fun findByName(name:String?): Optional<User?>? { // ... } }
Then any User
that is returned from findById
will be secured like other Spring Security-protected components:
- Java
-
@Autowired UserRepository users; @Test void getEmailWhenProxiedThenAuthorizes() { Optional<User> securedUser = users.findByName("name"); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail()); }
- Kotlin
-
import jdk.incubator.vector.VectorOperators.Test import java.nio.file.AccessDeniedException import java.util.* @Autowired var users:UserRepository? = null @Test fun getEmailWhenProxiedThenAuthorizes() { val securedUser: Optional<User> = users.findByName("name") assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()} }
@AuthorizeReturnObject
can be placed at the class level. Note, though, that this means Spring Security will attempt to proxy any return object, including String
, Integer
and other types.
This is often not what you want to do.
If you want to use @AuthorizeReturnObject
on a class or interface whose methods return value types, like int
, String
, Double
or collections of those types, then you should also publish the appropriate AuthorizationAdvisorProxyFactory.TargetVisitor
as follows:
- Java
-
@Bean static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() { return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes()); }
- Kotlin
-
@Bean open fun skipValueTypes() = Customizer<AuthorizationAdvisorProxyFactory> { it.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes()) }
Tip
|
You can set your own |
You can also programmatically proxy a given object.
To achieve this, you can autowire the provided AuthorizationProxyFactory
instance, which is based on which method security interceptors you have configured.
If you are using @EnableMethodSecurity
, then this means that it will by default have the interceptors for @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
.
You can proxy an instance of user in the following way:
- Java
-
@Autowired AuthorizationProxyFactory proxyFactory; @Test void getEmailWhenProxiedThenAuthorizes() { User user = new User("name", "email"); assertThat(user.getEmail()).isNotNull(); User securedUser = proxyFactory.proxy(user); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail); }
- Kotlin
-
@Autowired var proxyFactory:AuthorizationProxyFactory? = null @Test fun getEmailWhenProxiedThenAuthorizes() { val user: User = User("name", "email") assertThat(user.getEmail()).isNotNull() val securedUser: User = proxyFactory.proxy(user) assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail) }
You can also define your own instance if you need something different from the Spring Security default.
For example, if you define an AuthorizationProxyFactory
instance like so:
- Java
-
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize; // ... AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults(); // and if needing to skip value types proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
- Kotlin
-
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize // ... val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize()) // and if needing to skip value types proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())
Then you can wrap any instance of User
as follows:
- Java
-
@Test void getEmailWhenProxiedThenAuthorizes() { AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults(); User user = new User("name", "email"); assertThat(user.getEmail()).isNotNull(); User securedUser = proxyFactory.proxy(user); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail); }
- Kotlin
-
@Test fun getEmailWhenProxiedThenAuthorizes() { val proxyFactory: AuthorizationProxyFactory = AuthorizationAdvisorProxyFactory.withDefaults() val user: User = User("name", "email") assertThat(user.getEmail()).isNotNull() val securedUser: User = proxyFactory.proxy(user) assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail) }
AuthorizationProxyFactory
supports Java collections, streams, arrays, optionals, and iterators by proxying the element type and maps by proxying the value type.
This means that when proxying a List
of objects, the following also works:
- Java
-
@Test void getEmailWhenProxiedThenAuthorizes() { AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults(); List<User> users = List.of(ada, albert, marie); List<User> securedUsers = proxyFactory.proxy(users); securedUsers.forEach((securedUser) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail)); }
In limited circumstances, it may be valuable to proxy a Class
itself, and AuthorizationProxyFactory
also supports this.
This is roughly the equivalent of calling ProxyFactory#getProxyClass
in Spring Framework’s support for creating proxies.
One place where this is handy is when you need to construct the proxy class ahead-of-time, like with Spring AOT.
AuthorizationProxyFactory
supports whichever method security annotations are enabled in your application.
It is based off of whatever AuthorizationAdvisor
classes are published as a bean.
Since @EnableMethodSecurity
publishes @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
advisors by default, you will typically need to do nothing to activate the ability.
Note
|
SpEL expressions that use |
If you have security advice that you also want applied, you can publish your own AuthorizationAdvisor
like so:
- Java
-
@EnableMethodSecurity class SecurityConfig { @Bean static AuthorizationAdvisor myAuthorizationAdvisor() { return new AuthorizationAdvisor(); } }
- Kotlin
-
@EnableMethodSecurity internal class SecurityConfig { @Bean fun myAuthorizationAdvisor(): AuthorizationAdvisor { return AuthorizationAdvisor() } ]
And Spring Security will add that advisor into the set of advice that AuthorizationProxyFactory
adds when proxying an object.
One powerful use of this feature is to return a secured value from a controller like so:
- Java
-
@RestController public class UserController { @Autowired AuthorizationProxyFactory proxyFactory; @GetMapping User currentUser(@AuthenticationPrincipal User user) { return this.proxyFactory.proxy(user); } }
- Kotlin
-
@RestController class UserController { @Autowired var proxyFactory: AuthorizationProxyFactory? = null @GetMapping fun currentUser(@AuthenticationPrincipal user:User?): User { return proxyFactory.proxy(user) } }
You will need to add a MethodAuthorizationDeniedHandler
like this one:
- Java
-
@Component public class Null implements MethodAuthorizationDeniedHandler { @Override public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { return null; } } // ... @HandleAuthorizationDenied(handlerClass = Null.class) public class User { ... }
- Kotlin
-
@Component class Null : MethodAuthorizationDeniedHandler { override fun handleDeniedInvocation(methodInvocation: MethodInvocation?, authorizationResult: AuthorizationResult?): Any? { return null } } // ... @HandleAuthorizationDenied(handlerClass = Null.class) open class User { ... }
Then, you’ll see a different JSON serialization based on the authorization level of the user.
If they don’t have the user:read
authority, then they’ll see:
{
"name" : "name",
"email" : null
}
And if they do have that authority, they’ll see:
{
"name" : "name",
"email" : "email"
}
Tip
|
You can also add the Spring Boot property |
Spring Security will scan all beans in the application context for methods that use @AuthorizeReturnObject
.
When it finds one, it will create and register the appropriate proxy class ahead of time.
It will also recursively search for other nested objects that also use @AuthorizeReturnObject
and register them accordingly.
For example, consider the following Spring Boot application:
- Java
-
@SpringBootApplication public class MyApplication { @RestController public static class MyController { (1) @GetMapping @AuthorizeReturnObject Message getMessage() { (2) return new Message(someUser, "hello!"); } } public static class Message { (3) User to; String text; // ... @AuthorizeReturnObject public User getTo() { (4) return this.to; } // ... } public static class User { (5) // ... } public static void main(String[] args) { SpringApplication.run(MyApplication.class); } }
- Kotlin
-
@SpringBootApplication open class MyApplication { @RestController open class MyController { (1) @GetMapping @AuthorizeReturnObject fun getMessage():Message { (2) return Message(someUser, "hello!") } } open class Message { (3) val to: User val test: String // ... @AuthorizeReturnObject fun getTo(): User { (4) return this.to } // ... } open class User { (5) // ... } fun main(args: Array<String>) { SpringApplication.run(MyApplication.class) } }
-
- First, Spring Security finds the
MyController
bean -
- Finding a method that uses
@AuthorizeReturnObject
, it proxiesMessage
, the return value, and registers that proxy class toRuntimeHints
-
- Then, it traverses
Message
to see if it uses@AuthorizeReturnObject
-
- Finding a method that uses
@AuthorizeReturnObject
, it proxiesUser
, the return value, and registers that proxy class toRuntimeHints
-
- Finally, it traverses
User
to see if it uses@AuthorizeReturnObject
; finding nothing, the algorithm completes
There will be many times when Spring Security cannot determine the proxy class ahead of time since it may be hidden in an erased generic type.
Consider the following change to MyController
:
- Java
-
@RestController public static class MyController { @GetMapping @AuthorizeReturnObject List<Message> getMessages() { return List.of(new Message(someUser, "hello!")); } }
- Kotlin
-
@RestController static class MyController { @AuthorizeReturnObject @GetMapping fun getMessages(): Array<Message> = arrayOf(Message(someUser, "hello!")) }
In this case, the generic type is erased and so it isn’t apparent to Spring Security ahead-of-time that Message
will need to be proxied at runtime.
To address this, you can publish AuthorizeProxyFactoryHintsRegistrar
like so:
- Java
-
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) { return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class); }
- Kotlin
-
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar { return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java) }
Spring Security will register that class and then traverse its type as before.
There are some scenarios where you may not wish to throw an AuthorizationDeniedException
when a method is invoked without the required permissions.
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where authorization denied happened before invoking the method.
Spring Security provides support for handling authorization denied on method invocation by using the javadoc:org.springframework.security.authorization.method.HandleAuthorizationDenied[format=annotation].
The handler works for denied authorizations that happened in the @PreAuthorize
and @PostAuthorize
annotations as well as javadoc:org.springframework.security.authorization.AuthorizationDeniedException[] thrown from the method invocation itself.
Let’s consider the example from the previous section, but instead of creating the AccessDeniedExceptionInterceptor
to transform an AccessDeniedException
to a null
return value, we will use the handlerClass
attribute from @HandleAuthorizationDenied
:
- Java
-
public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1) @Override public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { return null; } } @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean (2) public NullMethodAuthorizationDeniedHandler nullMethodAuthorizationDeniedHandler() { return new NullMethodAuthorizationDeniedHandler(); } } public class User { // ... @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class) public String getEmail() { return this.email; } }
- Kotlin
-
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { (1) override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any { return null } } @Configuration @EnableMethodSecurity class SecurityConfig { @Bean (2) fun nullMethodAuthorizationDeniedHandler(): NullMethodAuthorizationDeniedHandler { return MaskMethodAuthorizationDeniedHandler() } } class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) (3)
-
Create an implementation of
MethodAuthorizationDeniedHandler
that returns anull
value -
Register the
NullMethodAuthorizationDeniedHandler
as a bean -
Annotate the method with
@HandleAuthorizationDenied
and pass theNullMethodAuthorizationDeniedHandler
to thehandlerClass
attribute
And then you can verify that a null
value is returned instead of the AccessDeniedException
:
Tip
|
You can also annotate your class with |
- Java
-
@Autowired UserRepository users; @Test void getEmailWhenProxiedThenNullEmail() { Optional<User> securedUser = users.findByName("name"); assertThat(securedUser.get().getEmail()).isNull(); }
- Kotlin
-
@Autowired var users:UserRepository? = null @Test fun getEmailWhenProxiedThenNullEmail() { val securedUser: Optional<User> = users.findByName("name") assertThat(securedUser.get().getEmail()).isNull() }
There are some scenarios where you might want to return a secure result derived from the denied result. For example, if a user is not authorized to see email addresses, you might want to apply some masking on the original email address, i.e. [email protected] would become use******@example.com.
For those scenarios, you can override the handleDeniedInvocationResult
from the MethodAuthorizationDeniedHandler
, which has the javadoc:org.springframework.security.authorization.method.MethodInvocationResult[] as an argument.
Let’s continue with the previous example, but instead of returning null
, we will return a masked value of the email:
- Java
-
public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1) @Override public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { return "***"; } @Override public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { String email = (String) methodInvocationResult.getResult(); return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*"); } } @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean (2) public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() { return new EmailMaskingMethodAuthorizationDeniedHandler(); } } public class User { // ... @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class) public String getEmail() { return this.email; } }
- Kotlin
-
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any { return "***" } override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any { val email = methodInvocationResult.result as String return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*") } } @Configuration @EnableMethodSecurity class SecurityConfig { @Bean fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler { return EmailMaskingMethodAuthorizationDeniedHandler() } } class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) (3)
-
Create an implementation of
MethodAuthorizationDeniedHandler
that returns a masked value of the unauthorized result value -
Register the
EmailMaskingMethodAuthorizationDeniedHandler
as a bean -
Annotate the method with
@HandleAuthorizationDenied
and pass theEmailMaskingMethodAuthorizationDeniedHandler
to thehandlerClass
attribute
And then you can verify that a masked email is returned instead of an AccessDeniedException
:
Warning
|
Since you have access to the original denied value, make sure that you correctly handle it and do not return it to the caller. |
- Java
-
@Autowired UserRepository users; @Test void getEmailWhenProxiedThenMaskedEmail() { Optional<User> securedUser = users.findByName("name"); // email is [email protected] assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com"); }
- Kotlin
-
@Autowired var users:UserRepository? = null @Test fun getEmailWhenProxiedThenMaskedEmail() { val securedUser: Optional<User> = users.findByName("name") // email is [email protected] assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com") }
When implementing the MethodAuthorizationDeniedHandler
you have a few options on what type you can return:
-
A
null
value. -
A non-null value, respecting the method’s return type.
-
Throw an exception, usually an instance of
AuthorizationDeniedException
. This is the default behavior. -
A
Mono
type for reactive applications.
Note that since the handler must be registered as beans in your application context, you can inject dependencies into them if you need a more complex logic.
In addition to that, you have available the MethodInvocation
or the MethodInvocationResult
, as well as the AuthorizationResult
for more details related to the authorization decision.
Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler for each of those methods, although it is perfectly fine to do that.
In such cases, we can use the information passed via parameters to decide what to do.
For example, we can create a custom @Mask
annotation and a handler that detects that annotation to decide what mask value to return:
- Java
-
import org.springframework.core.annotation.AnnotationUtils; @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface Mask { String value(); } public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler { @Override public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class); return mask.value(); } } @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public MaskAnnotationDeniedHandler maskAnnotationDeniedHandler() { return new MaskAnnotationDeniedHandler(); } } @Component public class MyService { @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class) @Mask("***") public String foo() { return "foo"; } @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class) @Mask("???") public String bar() { return "bar"; } }
- Kotlin
-
import org.springframework.core.annotation.AnnotationUtils @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class Mask(val value: String) class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler { override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any { val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java) return mask.value } } @Configuration @EnableMethodSecurity class SecurityConfig { @Bean fun maskAnnotationDeniedHandler(): MaskAnnotationDeniedHandler { return MaskAnnotationDeniedHandler() } } @Component class MyService { @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class) @Mask("***") fun foo(): String { return "foo" } @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class) @Mask("???") fun bar(): String { return "bar" } }
Now the return values when access is denied will be decided based on the @Mask
annotation:
- Java
-
@Autowired MyService myService; @Test void fooWhenDeniedThenReturnStars() { String value = this.myService.foo(); assertThat(value).isEqualTo("***"); } @Test void barWhenDeniedThenReturnQuestionMarks() { String value = this.myService.foo(); assertThat(value).isEqualTo("???"); }
- Kotlin
-
@Autowired var myService: MyService @Test fun fooWhenDeniedThenReturnStars() { val value: String = myService.foo() assertThat(value).isEqualTo("***") } @Test fun barWhenDeniedThenReturnQuestionMarks() { val value: String = myService.foo() assertThat(value).isEqualTo("???") }
You can also combine the @HandleAuthorizationDenied
with other annotations in order to reduce and simplify the annotations in a method.
Let’s consider the example from the previous section and merge @HandleAuthorizationDenied
with @Mask
:
- Java
-
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class) public @interface Mask { String value(); } @Mask("***") public String myMethod() { // ... }
- Kotlin
-
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class) annotation class Mask(val value: String) @Mask("***") fun myMethod(): String { // ... }
Now you do not have to remember to add both annotations when you need a mask behavior in your method. Make sure to read the Meta Annotations Support section for more details on the usage.
If you are using @EnableGlobalMethodSecurity
, you should migrate to @EnableMethodSecurity
.
Replace global method security with method security
javadoc:org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity[format=annotation] and <global-method-security>
are deprecated in favor of javadoc:org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity[@EnableMethodSecurity
] and <method-security>
, respectively.
The new annotation and XML element activate Spring’s pre-post annotations by default and use AuthorizationManager
internally.
This means that the following two listings are functionally equivalent:
- Java
-
@EnableGlobalMethodSecurity(prePostEnabled = true)
- Kotlin
-
@EnableGlobalMethodSecurity(prePostEnabled = true)
- Xml
-
<global-method-security pre-post-enabled="true"/>
and:
- Java
-
@EnableMethodSecurity
- Kotlin
-
@EnableMethodSecurity
- Xml
-
<method-security/>
For applications not using the pre-post annotations, make sure to turn it off to avoid activating unwanted behavior.
For example, a listing like:
- Java
-
@EnableGlobalMethodSecurity(securedEnabled = true)
- Kotlin
-
@EnableGlobalMethodSecurity(securedEnabled = true)
- Xml
-
<global-method-security secured-enabled="true"/>
should change to:
- Java
-
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
- Kotlin
-
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
- Xml
-
<method-security secured-enabled="true" pre-post-enabled="false"/>
As a performance optimization, a new method was introduced to MethodSecurityExpressionHandler
that takes a Supplier<Authentication>
instead of an Authentication
.
This allows Spring Security to defer the lookup of the Authentication
, and is taken advantage of automatically when you use @EnableMethodSecurity
instead of @EnableGlobalMethodSecurity
.
However, let’s say that your code extends DefaultMethodSecurityExpressionHandler
and overrides createSecurityExpressionRoot(Authentication, MethodInvocation)
to return a custom SecurityExpressionRoot
instance.
This will no longer work because the arrangement that @EnableMethodSecurity
sets up calls createEvaluationContext(Supplier<Authentication>, MethodInvocation)
instead.
Happily, such a level of customization is often unnecessary. Instead, you can create a custom bean with the authorization methods that you need.
For example, let’s say you are wanting a custom evaluation of @PostAuthorize("hasAuthority('ADMIN')")
.
You can create a custom @Bean
like this one:
- Java
-
class MyAuthorizer { boolean isAdmin(MethodSecurityExpressionOperations root) { boolean decision = root.hasAuthority("ADMIN"); // custom work ... return decision; } }
- Kotlin
-
class MyAuthorizer { fun isAdmin(val root: MethodSecurityExpressionOperations): boolean { val decision = root.hasAuthority("ADMIN"); // custom work ... return decision; } }
and then refer to it in the annotation like so:
- Java
-
@PreAuthorize("@authz.isAdmin(#root)")
- Kotlin
-
@PreAuthorize("@authz.isAdmin(#root)")
If you must continue subclassing DefaultMethodSecurityExpressionHandler
, you can still do so.
Instead, override the createEvaluationContext(Supplier<Authentication>, MethodInvocation)
method like so:
- Java
-
@Component class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler { @Override public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) { StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi); MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue(); MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate); context.setRootObject(root); return context; } }
- Kotlin
-
@Component class MyExpressionHandler: DefaultMethodSecurityExpressionHandler { override fun createEvaluationContext(val authentication: Supplier<Authentication>, val mi: MethodInvocation): EvaluationContext { val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext val delegate = context.getRootObject().getValue() as MethodSecurityExpressionOperations val root = MySecurityExpressionRoot(delegate) context.setRootObject(root) return context } }
Now that you have secured your application’s requests, please secure its requests if you haven’t already. You can also read further on testing your application or on integrating Spring Security with other aspects of you application like the data layer or tracing and metrics.