You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1599 lines
55 KiB
1599 lines
55 KiB
|
|
[[jc-method]] |
|
= Method Security |
|
:figures: servlet/authorization |
|
|
|
In addition to xref:servlet/authorization/authorize-http-requests.adoc[modeling authorization at the request level], Spring Security also supports modeling at the method level. |
|
|
|
[[activate-method-security]] |
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<sec:method-security/> |
|
---- |
|
==== |
|
|
|
Then, you are immediately able to annotate any Spring-managed class or method with <<use-preauthorize, `@PreAuthorize`>>, <<use-postauthorize,`@PostAuthorize`>>, <<use-prefilter,`@PreFilter`>>, and <<use-postfilter,`@PostFilter`>> to authorize method invocations, including the input parameters and return values. |
|
|
|
[NOTE] |
|
{spring-boot-reference-url}using.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 <<use-aspectj, AspectJ support>>, <<use-programmatic-authorization,custom annotations>>, and several configuration points. |
|
Consider learning about the following use cases: |
|
|
|
* <<migration-enableglobalmethodsecurity, Migrating from `@EnableGlobalMethodSecurity`>> |
|
* Understanding <<method-security-architecture,how method security works>> and reasons to use it |
|
* Comparing <<request-vs-method,request-level and method-level authorization>> |
|
* Authorizing methods with <<use-preauthorize,`@PreAuthorize`>> and <<use-postauthorize,`@PostAuthorize`>> |
|
* Filtering methods with <<use-prefilter,`@PreFilter`>> and <<use-postfilter,`@PostFilter`>> |
|
* Authorizing methods with <<use-jsr250,JSR-250 annotations>> |
|
* Authorizing methods with <<use-aspectj,AspectJ expressions>> |
|
* Integrating with <<weave-aspectj,AspectJ byte-code weaving>> |
|
* Customizing <<customizing-expression-handling,SpEL expression handling>> |
|
* Integrating with <<custom-authorization-managers,custom authorization systems>> |
|
|
|
[[method-security-architecture]] |
|
== How Method Security Works |
|
|
|
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. |
|
|
|
[[use-method-security]] |
|
[NOTE] |
|
==== |
|
This annotation and XML element supercede `@EnableGlobalMethodSecurity` and `<sec:global-method-security/>`, respectively. |
|
They offer the following improvements: |
|
|
|
1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters. |
|
This simplifies reuse and customization. |
|
2. Favors direct bean-based configuration, instead of requiring extending `GlobalMethodSecurityConfiguration` to customize beans |
|
3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize |
|
4. Checks for conflicting annotations to ensure an unambiguous security configuration |
|
5. Complies with JSR-250 |
|
6. Enables `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` by default |
|
|
|
If you are using `@EnableGlobalMethodSecurity` or `<global-method-security/>`, these are now deprecated, and you are encouraged to migrate. |
|
==== |
|
|
|
Method authorization is a combination of before- and after-method authorization. |
|
Consider a service bean that is annotated in the following way: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Service |
|
public class MyCustomerService { |
|
@PreAuthorize("hasAuthority('permission:read')") |
|
@PostAuthorize("returnObject.owner == authentication.name") |
|
public Customer readCustomer(String id) { ... } |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 <<activate-method-security,is activated>>: |
|
|
|
image::{figures}/methodsecurity.png[] |
|
|
|
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor/html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>> |
|
2. The interceptor invokes {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager#check`] |
|
3. The authorization manager uses a `MethodSecurityExpressionHandler` to parse the annotation's <<authorization-expressions,SpEL expression>> and constructs a corresponding `EvaluationContext` from a `MethodSecurityExpressionRoot` containing xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[a `Supplier<Authentication>`] and `MethodInvocation`. |
|
4. The interceptor uses this context to evaluate the expression; specifically, it reads xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] from the `Supplier` and checks whether it has `permission:read` in its collection of xref:servlet/authorization/architecture.adoc#authz-authorities[authorities] |
|
5. If the evaluation passes, then Spring AOP proceeds to invoke the method. |
|
6. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`] which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response |
|
7. After the method returns, Spring AOP invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[`AuthorizationManagerAfterMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PostAuthorize` pointcut>>, operating the same as above, but with {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[`PostAuthorizeAuthorizationManager`] |
|
8. If the evaluation passes (in this case, the return value belongs to the logged-in user), processing continues normally |
|
9. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`], which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] 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 |
|
|
|
[[unanimous-based-authorization-decisions]] |
|
=== Multiple Annotations Are Computed In Series |
|
|
|
As demonstrated above, if a method invocation involves multiple <<authorizing-with-annotations,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. |
|
|
|
[[repeated-annotations]] |
|
=== Repeated Annotations Are Not Supported |
|
|
|
That said, it is not supported to repeat the same annotation on the same method. |
|
For example, you cannot please `@PreAuthorize` twice on the same method. |
|
|
|
Instead, use SpEL's boolean support or its support for delegating to a separate bean. |
|
|
|
[[annotation-method-pointcuts]] |
|
=== Each Annotation Has Its Own Pointcut |
|
|
|
Each annotation has its own pointcut instance that looks for that annotation or its <<meta-annotations,meta-annotation>> counterparts across the entire object hierarchy, starting at <<class-or-interface-annotations,the method and its enclosing class>>. |
|
|
|
You can see the specifics of this in {security-api-url}org/springframework/security/authorization/method/AuthorizationMethodPointcuts.html[`AuthorizationMethodPointcuts`]. |
|
|
|
[[annotation-method-interceptors]] |
|
=== Each Annotation Has Its Own Method Interceptor |
|
|
|
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 <<_enabling_certain_annotations,publish only the `@PostAuthorize` method interceptor>>. |
|
|
|
The method interceptors are as follows: |
|
|
|
* For <<use-preauthorize,`@PreAuthorize`>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthenticationManagerBeforeMethodInterceptor#preAuthorize`], which in turn uses {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager`] |
|
* For <<use-postauthorize,`@PostAuthorize`>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[`AuthenticationManagerAfterMethodInterceptor#postAuthorize`], which in turn uses {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[`PostAuthorizeAuthorizationManager`] |
|
* For <<use-prefilter,`@PreFilter`>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.html[`PreFilterAuthorizationMethodInterceptor`] |
|
* For <<use-postfilter,`@PostFilter`>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.html[`PostFilterAuthorizationMethodInterceptor`] |
|
* For <<use-secured,`@Secured`>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthenticationManagerBeforeMethodInterceptor#secured`], which in turn uses {security-api-url}org/springframework/security/authorization/method/SecuredAuthorizationManager.html[`SecuredAuthorizationManager`] |
|
* For JSR-250 annotations, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthenticationManagerBeforeMethodInterceptor#jsr250`], which in turn uses {security-api-url}org/springframework/security/authorization/method/Jsr250AuthorizationManager.html[`Jsr250AuthorizationManager`] |
|
|
|
Generally speaking, you can consider the following listing as representative of what interceptors Spring Security publishes when you add `@EnableMethodSecurity`: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
==== |
|
|
|
[[favor-granting-authorities]] |
|
=== Favor Granting Authorities Over Complicated SpEL Expressions |
|
|
|
Quite often it can be tempting to introduce a complicated SpEL expression like the following: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')") |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="kotlin"] |
|
---- |
|
@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 |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
static RoleHierarchy roleHierarchy() { |
|
return new RoleHierarchyImpl("ROLE_ADMIN > permission:read"); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,java,role="secondary"] |
|
---- |
|
companion object { |
|
@Bean |
|
fun roleHierarchy(): RoleHierarchy { |
|
return RoleHierarchyImpl("ROLE_ADMIN > permission:read") |
|
} |
|
} |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl"> |
|
<constructor-arg value="ROLE_ADMIN > permission:read"/> |
|
</bean> |
|
---- |
|
==== |
|
|
|
and then <<customizing-expression-handling,set that in a `MethodSecurityExpressionHandler` instance>>. |
|
This then allows you to have a simpler <<use-preauthorize,`@PreAuthorize`>> expression like this one: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@PreAuthorize("hasAuthority('permission:read')") |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@PreAuthorize("hasAuthority('permission:read')") |
|
---- |
|
==== |
|
|
|
Or, where possible, adapt application-specific authorization logic into granted authorities at login time. |
|
|
|
[[request-vs-method]] |
|
== Comparing Request-level vs Method-level Authorization |
|
|
|
When should you favor method-level authorization over xref:servlet/authorization/authorize-http-requests.adoc[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 xref:servlet/authorization/authorize-http-requests.adoc#activate-request-security[a catch-all authorization rule] in your xref:servlet/configuration/java.adoc#jc-httpsecurity[`HttpSecurity`] instance. |
|
|
|
[[authorizing-with-annotations]] |
|
== Authorizing with Annotations |
|
|
|
The primary way Spring Security enables method-level authorization support is through annotations that you can add to methods, classes, and interfaces. |
|
|
|
[[use-preauthorize]] |
|
=== Authorizing Method Invocation with `@PreAuthorize` |
|
|
|
When <<activate-method-security,Method Security is active>>, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreAuthorize.html[`@PreAuthorize`] annotation like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Component |
|
public class BankService { |
|
@PreAuthorize("hasRole('ADMIN')") |
|
public Account readAccount(Long id) { |
|
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 xref:servlet/test/method.adoc[test the class] to confirm it is enforcing the authorization rule like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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 |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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-annotations, meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, SpEL Authorization Expressions>>. |
|
|
|
While `@PreAuthorize` is quite helpful for declaring needed authorities, it can also be used to evaluate more complex <<using_method_parameters,expressions that involve the method parameters>>. |
|
asdf |
|
The above two snippets are ensuring that the user can only request orders that belong to them by comparing the username parameter to xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication#getName`]. |
|
|
|
The result is that the above method will only be invoked if the `username` in the request path matches the logged-in user's `name`. |
|
If not, Spring Security will throw an `AccessDeniedException` and return a 403 status code. |
|
|
|
[[use-postauthorize]] |
|
=== Authorization Method Results with `@PostAuthorize` |
|
|
|
When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostAuthorize.html[`@PostAuthorize`] annotation like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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 |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 xref:servlet/test/method.adoc[test the class] to confirm it is enforcing the authorization rule: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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 |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, SpEL Authorization Expressions>>. |
|
|
|
`@PostAuthorize` is particularly helpful when defending against https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html[Insecure Direct Object Reference]. |
|
In fact, it can be defined as a <<meta-annotations,meta-annotation>> like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
|
@Retention(RetentionPolicy.RUNTIME) |
|
@PostAuthorize("returnObject.owner == authentication.name") |
|
public @interface RequireOwnership {} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
[source,java,role="primary"] |
|
---- |
|
@Component |
|
public class BankService { |
|
@RequireOwnership |
|
public Account readAccount(Long id) { |
|
// ... is only returned if the `Account` belongs to the logged in user |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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. |
|
|
|
[[use-prefilter]] |
|
=== Filtering Method Parameters with `@PreFilter` |
|
|
|
[NOTE] |
|
`@PreFilter` is not yet supported for Kotlin-specific data types; for that reason, only Java snippets are shown |
|
|
|
When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreFilter.html[`@PreFilter`] annotation like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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; |
|
} |
|
} |
|
---- |
|
==== |
|
|
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@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); |
|
} |
|
---- |
|
==== |
|
|
|
[TIP] |
|
`@PreFilter` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, 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 |
|
[source,java,role="primary"] |
|
---- |
|
@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) |
|
---- |
|
==== |
|
|
|
The result is that the above method will only have the `Account` instances where their `owner` attribute matches the logged-in user's `name`. |
|
|
|
[[use-postfilter]] |
|
=== Filtering Method Results with `@PostFilter` |
|
|
|
[NOTE] |
|
`@PostFilter` is not yet supported for Kotlin-specific data types; for that reason, only Java snippets are shown |
|
|
|
When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostFilter.html[`@PostFilter`] annotation like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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; |
|
} |
|
} |
|
---- |
|
==== |
|
|
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@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"); |
|
} |
|
---- |
|
==== |
|
|
|
[TIP] |
|
`@PostFilter` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, 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 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) |
|
``` |
|
|
|
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 xref:servlet/integrations/data.adoc[filter the data in the data layer] instead. |
|
|
|
[[use-secured]] |
|
=== Authorizing Method Invocation with `@Secured` |
|
|
|
{security-api-url}org/springframework/security/access/annotation/Secured.html[`@Secured`] is a legacy option for authorizing invocations. |
|
<<use-preauthorize,`@PreAuthorize`>> supercedes it and is recommended instead. |
|
|
|
To use the `@Secured` annotation, you should first change your Method Security declaration to enable it like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity(securedEnabled = true) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity(securedEnabled = true) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<sec:method-security secured-enabled="true"/> |
|
---- |
|
==== |
|
|
|
This will cause Spring Security to publish <<annotation-method-interceptors,the corresponding method interceptor>> that authorizes methods, classes, and interfaces annotated with `@Secured`. |
|
|
|
[[use-jsr250]] |
|
=== Authorizing Method Invocation with JSR-250 Annotations |
|
|
|
In case you would like to use https://jcp.org/en/jsr/detail?id=250[JSR-250] annotations, Spring Security also supports that. |
|
<<use-preauthorize,`@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 |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity(jsr250Enabled = true) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity(jsr250Enabled = true) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<sec:method-security jsr250-enabled="true"/> |
|
---- |
|
==== |
|
|
|
This will cause Spring Security to publish <<annotation-method-interceptors,the corresponding method interceptor>> that authorizes methods, classes, and interfaces annotated with `@RolesAllowed`, `@PermitAll`, and `@DenyAll`. |
|
|
|
|
|
[[class-or-interface-annotations]] |
|
=== Declaring Annotations at the Class or Interface Level |
|
|
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@Controller |
|
@PreAuthorize("hasAuthority('ROLE_USER')") |
|
public class MyController { |
|
@GetMapping("/endpoint") |
|
public String endpoint() { ... } |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
[source,java,role="primary"] |
|
---- |
|
@Controller |
|
@PreAuthorize("hasAuthority('ROLE_USER')") |
|
public class MyController { |
|
@GetMapping("/endpoint") |
|
@PreAuthorize("hasAuthority('ROLE_ADMIN')") |
|
public String endpoint() { ... } |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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. |
|
|
|
[[meta-annotations]] |
|
=== Using Meta Annotations |
|
|
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
|
@Retention(RetentionPolicy.RUNTIME) |
|
@PreAuthorize("hasRole('ADMIN')") |
|
public @interface IsAdmin {} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
[source,java,role="primary"] |
|
---- |
|
@Component |
|
public class BankService { |
|
@IsAdmin |
|
public Account readAccount(Long id) { |
|
// ... is only returned if the `Account` belongs to the logged in user |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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. |
|
|
|
[[enable-annotation]] |
|
=== Enabling Certain Annotations |
|
|
|
You can turn off ``@EnableMethodSecurity``'s pre-configuration and replace it with you own. |
|
You may choose to do this if you want to <<custom-authorization-managers,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: |
|
|
|
.Only @PostAuthorize Configuration |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableMethodSecurity(prePostEnabled = false) |
|
class MethodSecurityConfig { |
|
@Bean |
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
|
Advisor postAuthorize() { |
|
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize(); |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Configuration |
|
@EnableMethodSecurity(prePostEnabled = false) |
|
class MethodSecurityConfig { |
|
@Bean |
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
|
fun postAuthorize() : Advisor { |
|
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize() |
|
} |
|
} |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 <<annotation-method-interceptors, the `@PostAuthorize` interceptor>> itself. |
|
|
|
[[use-intercept-methods]] |
|
== Authorizing with `<intercept-methods>` |
|
|
|
While using Spring Security's <<authorizing-with-annotations,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 xref:servlet/appendix/namespace/method-security.adoc#nsa-intercept-methods[`<intercept-methods>`] like so: |
|
|
|
==== |
|
.Xml |
|
[source,xml,role="primary"] |
|
---- |
|
<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, <<authorizing-with-annotations,use annotation support>> instead. |
|
|
|
[[use-programmatic-authorization]] |
|
== Authorizing Methods Programmatically |
|
|
|
As you've already seen, there are several ways that you can specify non-trivial authorization rules using <<authorization-expressions, Method Security SpEL expressions>>. |
|
|
|
There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based. |
|
This gives use access the entire Java language for increased testability and flow control. |
|
|
|
=== Using a Custom Bean in SpEL |
|
|
|
The first way to authorize a method programmatically is a two-step process. |
|
|
|
First, declare a bean that has a method that takes a `MethodSecurityExpressionOperations` instance like the following: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Component("authz") |
|
public class AuthorizationLogic { |
|
public boolean decide(MethodSecurityExpressionOperations operations) { |
|
// ... authorization logic |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
[source,java,role="primary"] |
|
---- |
|
@Controller |
|
public class MyController { |
|
@PreAuthorize("@authz.decide(#root)") |
|
@GetMapping("/endpoint") |
|
public String endpoint() { |
|
// ... |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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. |
|
|
|
[[custom-authorization-managers]] |
|
=== Using a Custom Authorization Manager |
|
|
|
The second way to authorize a method programmatically is two create a custom xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[`AuthorizationManager`]. |
|
|
|
First, declare an authorization manager instance, perhaps like this one: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Component |
|
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation> { |
|
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) { |
|
// ... authorization logic |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Component("authz") |
|
open class MyAuthorizationManager: AuthorizationManager<MethodInvocation> { |
|
fun check(val authentication: Supplier<Authentication>, val invocation: MethodInvocation): 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: |
|
|
|
.Only @PostAuthorize Configuration |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableMethodSecurity(prePostEnabled = false) |
|
class MethodSecurityConfig { |
|
@Bean |
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
|
Advisor postAuthorize(MyAuthorizationManager manager) { |
|
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager); |
|
} |
|
|
|
@Bean |
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
|
Advisor postAuthorize(MyAuthorizationManager manager) { |
|
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager); |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
[source,xml,role="secondary"] |
|
---- |
|
<sec:method-security pre-post-enabled="false"/> |
|
|
|
<aop:config/> |
|
|
|
<bean id="postAuthorize" |
|
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 `AuthorizationInterceptorsOrder`. |
|
==== |
|
|
|
[[customizing-expression-handling]] |
|
=== Customizing Expression Handling |
|
|
|
Or, third, you can customize how each SpEL expression is handled. |
|
To do that, you can expose a custom {security-api-url}org.springframework.security.access.expression.method.MethodSecurityExpressionHandler.html[`MethodSecurityExpressionHandler`], like so: |
|
|
|
.Custom MethodSecurityExpressionHandler |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { |
|
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); |
|
handler.setRoleHierarchy(roleHierarchy); |
|
return handler; |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
companion object { |
|
@Bean |
|
fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler { |
|
val handler = DefaultMethodSecurityExpressionHandler(); |
|
handler.setRoleHierarchy(roleHierarchy); |
|
return handler; |
|
} |
|
} |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 `MethodSecurityExpressionHandler` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes |
|
==== |
|
|
|
You can also <<subclass-defaultmethodsecurityexpressionhandler,subclass `DefaultMessageSecurityExpressionHandler`>> to add your own custom authorization expressions beyond the defaults. |
|
|
|
[[use-aspectj]] |
|
== Authorizing with AspectJ |
|
|
|
[[match-by-pointcut]] |
|
=== Matching Methods with Custom Pointcuts |
|
|
|
Being built on Spring AOP, you can declare patterns that are not related to annotations, similar to xref:servlet/authorization/authorize-http-requests.adoc[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 xref:servlet/appendix/namespace/method-security.adoc#nsa-protect-pointcut[`<protect-pointcut>`] to match AOP expressions to authorization rules for your service layer like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; |
|
|
|
@Bean |
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
|
static Advisor protectServicePointcut() { |
|
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); |
|
pattern.setPattern("execution(* com.mycompany.*Service.*(..))"); |
|
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER")); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; |
|
|
|
companion object { |
|
@Bean |
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
|
fun protectServicePointcut(): Advisor { |
|
var pattern = JdkRegexpMethodPointcut(); |
|
pattern.setPattern("execution(* com.mycompany.*Service.*(..))"); |
|
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER")); |
|
} |
|
} |
|
---- |
|
|
|
[source,xml] |
|
---- |
|
<sec:method-security> |
|
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/> |
|
</sec:method-security> |
|
---- |
|
==== |
|
|
|
[[weave-aspectj]] |
|
=== Integrate with AspectJ Byte-weaving |
|
|
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<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. |
|
|
|
[[authorization-expressions]] |
|
== Expressing Authorization with SpEL |
|
|
|
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. |
|
|
|
[[using-authorization-expression-fields-and-methods]] |
|
=== Using Authorization Expression Fields and Methods |
|
|
|
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, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session |
|
* `denyAll` - The method is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session |
|
* `hasAuthority` - The method requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value |
|
* `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix |
|
* `hasAnyAuthority` - The method requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values |
|
* `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix |
|
* `hasPermission` - A hook into your `PermissionEvaluator` instance for doing object-level authorization |
|
|
|
And here is a brief look at the most common fields: |
|
|
|
* `authentication` - The `Authentication` instance associated with this method invocation |
|
* `principal` - The `Authentication#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: |
|
|
|
.Authorize Requests |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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 |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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 |
|
[source,xml,role="secondary"] |
|
---- |
|
<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> |
|
---- |
|
==== |
|
<1> This method may not be invoked by anyone for any reason |
|
<2> This method may only be invoked by ``Authentication``s granted the `ROLE_ADMIN` authority |
|
<3> This method may only be invoked by ``Authentication``s granted the `db` and `ROLE_ADMIN` authorities |
|
<4> This method may only be invoked by ``Princpal``s with an `aud` claim equal to "my-audience" |
|
<5> This method may only be invoked if the bean ``authz``'s `check` method returns `true` |
|
|
|
[[using_method_parameters]] |
|
=== Using Method Parameters |
|
|
|
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. |
|
|
|
1. 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 |
|
[source,java,role="primary"] |
|
---- |
|
import org.springframework.security.access.method.P; |
|
|
|
... |
|
|
|
@PreAuthorize("hasPermission(#c, 'write')") |
|
public void updateContact(@P("c") Contact contact); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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` have `write` permission specifically for this `Contact` instance. |
|
+ |
|
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. |
|
|
|
* If xref:servlet/integrations/data.adoc[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 |
|
[source,java,role="primary"] |
|
---- |
|
import org.springframework.data.repository.query.Param; |
|
|
|
... |
|
|
|
@PreAuthorize("#n == authentication.name") |
|
Contact findContactByName(@Param("n") String name); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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 to `Authentication#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. |
|
|
|
[[migration-enableglobalmethodsecurity]] |
|
== Migrating from `@EnableGlobalMethodSecurity` |
|
|
|
If you are using `@EnableGlobalMethodSecurity`, you should migrate to `@EnableMethodSecurity`. |
|
|
|
[[servlet-replace-globalmethodsecurity-with-methodsecurity]] |
|
=== Replace xref:servlet/authorization/method-security.adoc#jc-enable-global-method-security[global method security] with xref:servlet/authorization/method-security.adoc#jc-enable-method-security[method security] |
|
|
|
{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[`@EnableGlobalMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[`<global-method-security>`] are deprecated in favor of {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[`@EnableMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[`<method-security>`], respectively. |
|
The new annotation and XML element activate Spring's xref:servlet/authorization/method-security.adoc#jc-enable-method-security[pre-post annotations] by default and use `AuthorizationManager` internally. |
|
|
|
This means that the following two listings are functionally equivalent: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableGlobalMethodSecurity(prePostEnabled = true) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableGlobalMethodSecurity(prePostEnabled = true) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<global-method-security pre-post-enabled="true"/> |
|
---- |
|
==== |
|
|
|
and: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 |
|
[source,java,role="primary"] |
|
---- |
|
@EnableGlobalMethodSecurity(securedEnabled = true) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableGlobalMethodSecurity(securedEnabled = true) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<global-method-security secured-enabled="true"/> |
|
---- |
|
==== |
|
|
|
should change to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<method-security secured-enabled="true" pre-post-enabled="false"/> |
|
---- |
|
==== |
|
|
|
=== Use a Custom `@Bean` instead of subclassing `DefaultMethodSecurityExpressionHandler` |
|
|
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
class MyAuthorizer { |
|
boolean isAdmin(MethodSecurityExpressionOperations root) { |
|
boolean decision = root.hasAuthority("ADMIN"); |
|
// custom work ... |
|
return decision; |
|
} |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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 |
|
[source,java,role="primary"] |
|
---- |
|
@PreAuthorize("@authz.isAdmin(#root)") |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@PreAuthorize("@authz.isAdmin(#root)") |
|
---- |
|
==== |
|
|
|
[[subclass-defaultmethodsecurityexpressionhandler]] |
|
==== I'd still prefer to subclass `DefaultMethodSecurityExpressionHandler` |
|
|
|
If you must continue subclassing `DefaultMethodSecurityExpressionHandler`, you can still do so. |
|
Instead, override the `createEvaluationContext(Supplier<Authentication>, MethodInvocation)` method like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@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 |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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; |
|
} |
|
} |
|
---- |
|
==== |
|
|
|
== Further Reading |
|
|
|
Now that you have secured your application's requests, please xref:servlet/authorization/authorize-http-requests.adoc[secure its requests] if you haven't already. |
|
You can also read further on xref:servlet/test/index.adoc[testing your application] or on integrating Spring Security with other aspects of you application like xref:servlet/integrations/data.adoc[the data layer] or xref:servlet/integrations/observability.adoc[tracing and metrics].
|
|
|