mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-02-26 09:24:53 +00:00
1783 lines
58 KiB
Plaintext
1783 lines
58 KiB
Plaintext
|
|
[[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:
|
|
|
|
[tabs]
|
|
======
|
|
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>>
|
|
* Coordinating with <<changing-the-order,@Transactional and other AOP-based annotations>>
|
|
* 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:
|
|
|
|
[tabs]
|
|
======
|
|
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 place `@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`:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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>>.
|
|
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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 to create a custom xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[`AuthorizationManager`].
|
|
|
|
First, declare an authorization manager instance, perhaps like this one:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,role="primary"]
|
|
----
|
|
@Component
|
|
public class 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::
|
|
+
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@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:
|
|
|
|
.Only @PreAuthorize and @PostAuthorize Configuration
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,role="primary"]
|
|
----
|
|
@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::
|
|
+
|
|
[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="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 `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
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,role="primary"]
|
|
----
|
|
@Bean
|
|
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
|
|
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
|
handler.setRoleHierarchy(roleHierarchy);
|
|
return handler;
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
companion object {
|
|
@Bean
|
|
fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
|
|
val handler = DefaultMethodSecurityExpressionHandler();
|
|
handler.setRoleHierarchy(roleHierarchy);
|
|
return handler;
|
|
}
|
|
}
|
|
----
|
|
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,role="primary"]
|
|
----
|
|
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::
|
|
+
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
|
|
|
|
companion object {
|
|
@Bean
|
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
fun protectServicePointcut(): Advisor {
|
|
var pattern = AspectJExpressionPointcut();
|
|
pattern.setExpression("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:
|
|
|
|
[tabs]
|
|
======
|
|
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.
|
|
|
|
[[changing-the-order]]
|
|
== Specifying Order
|
|
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,role="primary"]
|
|
----
|
|
@EnableTransactionManagement(order = 0)
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@EnableTransactionManagement(order = 0)
|
|
----
|
|
|
|
Xml::
|
|
+
|
|
[source,xml,role="secondary"]
|
|
----
|
|
<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.
|
|
|
|
[[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
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
+
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
+
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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:
|
|
|
|
[tabs]
|
|
======
|
|
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].
|