526 lines
18 KiB
Plaintext
526 lines
18 KiB
Plaintext
|
|
[[el-access]]
|
|
= Expression-Based Access Control
|
|
Spring Security 3.0 introduced the ability to use Spring Expression Language (SpEL) expressions as an authorization mechanism in addition to the existing configuration attributes and access-decision voters.
|
|
Expression-based access control is built on the same architecture but lets complicated Boolean logic be encapsulated in a single expression.
|
|
|
|
|
|
== Overview
|
|
Spring Security uses SpEL for expression support and you should look at how that works if you are interested in understanding the topic in more depth.
|
|
Expressions are evaluated with a "`root object`" as part of the evaluation context.
|
|
Spring Security uses specific classes for web and method security as the root object to provide built-in expressions and access to values, such as the current principal.
|
|
|
|
[[el-common-built-in]]
|
|
=== Common Built-In Expressions
|
|
The base class for expression root objects is `SecurityExpressionRoot`.
|
|
This provides some common expressions that are available in both web and method security:
|
|
|
|
[[common-expressions]]
|
|
.Common built-in expressions
|
|
|===
|
|
| Expression | Description
|
|
|
|
| `hasRole(String role)`
|
|
| Returns `true` if the current principal has the specified role.
|
|
|
|
Example: `hasRole('admin')`
|
|
|
|
By default, if the supplied role does not start with `ROLE_`, it is added.
|
|
You can customize this behavior by modifying the `defaultRolePrefix` on `DefaultWebSecurityExpressionHandler`.
|
|
|
|
| `hasAnyRole(String... roles)`
|
|
| Returns `true` if the current principal has any of the supplied roles (given as a comma-separated list of strings).
|
|
|
|
Example: `hasAnyRole('admin', 'user')`.
|
|
|
|
By default, if the supplied role does not start with `ROLE_`, it is added.
|
|
You can customize this behavior by modifying the `defaultRolePrefix` on `DefaultWebSecurityExpressionHandler`.
|
|
|
|
| `hasAuthority(String authority)`
|
|
| Returns `true` if the current principal has the specified authority.
|
|
|
|
Example: `hasAuthority('read')`
|
|
|
|
| `hasAnyAuthority(String... authorities)`
|
|
| Returns `true` if the current principal has any of the supplied authorities (given as a comma-separated list of strings).
|
|
|
|
Example: `hasAnyAuthority('read', 'write')`.
|
|
|
|
| `principal`
|
|
| Allows direct access to the principal object that represents the current user.
|
|
|
|
| `authentication`
|
|
| Allows direct access to the current `Authentication` object obtained from the `SecurityContext`.
|
|
|
|
| `permitAll`
|
|
| Always evaluates to `true`.
|
|
|
|
| `denyAll`
|
|
| Always evaluates to `false`.
|
|
|
|
| `isAnonymous()`
|
|
| Returns `true` if the current principal is an anonymous user.
|
|
|
|
| `isRememberMe()`
|
|
| Returns `true` if the current principal is a remember-me user.
|
|
|
|
| `isAuthenticated()`
|
|
| Returns `true` if the user is not anonymous.
|
|
|
|
| `isFullyAuthenticated()`
|
|
| Returns `true` if the user is not an anonymous and is not a remember-me user.
|
|
|
|
| `hasPermission(Object target, Object permission)`
|
|
| Returns `true` if the user has access to the provided target for the given permission.
|
|
Example, `hasPermission(domainObject, 'read')`.
|
|
|
|
| `hasPermission(Object targetId, String targetType, Object permission)`
|
|
| Returns `true` if the user has access to the provided target for the given permission.
|
|
Example, `hasPermission(1, 'com.example.domain.Message', 'read')`.
|
|
|===
|
|
|
|
|
|
|
|
[[el-access-web]]
|
|
== Web Security Expressions
|
|
To use expressions to secure individual URLs, you first need to set the `use-expressions` attribute in the `<http>` element to `true`.
|
|
Spring Security then expects the `access` attributes of the `<intercept-url>` elements to contain SpEL expressions.
|
|
Each expression should evaluate to a Boolean, defining whether access should be allowed or not.
|
|
The following listing shows an example:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<http>
|
|
<intercept-url pattern="/admin*"
|
|
access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
|
|
...
|
|
</http>
|
|
----
|
|
====
|
|
|
|
Here, we have defined that the `admin` area of an application (defined by the URL pattern) should be available only to users who have the granted authority (`admin`) and whose IP address matches a local subnet.
|
|
We have already seen the built-in `hasRole` expression in the previous section.
|
|
The `hasIpAddress` expression is an additional built-in expression that is specific to web security.
|
|
It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluating web-access expressions.
|
|
This object also directly exposed the `HttpServletRequest` object under the name `request` so that you can invoke the request directly in an expression.
|
|
If expressions are being used, a `WebExpressionVoter` is added to the `AccessDecisionManager` that is used by the namespace.
|
|
So, if you do not use the namespace and want to use expressions, you have to add one of these to your configuration.
|
|
|
|
[[el-access-web-beans]]
|
|
=== Referring to Beans in Web Security Expressions
|
|
|
|
If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose.
|
|
For example, you could use the following, assuming you have a Bean with the name of `webSecurity` that contains the following method signature:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
public class WebSecurity {
|
|
public boolean check(Authentication authentication, HttpServletRequest request) {
|
|
...
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
class WebSecurity {
|
|
fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean {
|
|
// ...
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
You could then refer to the method as follows:
|
|
|
|
.Refer to method
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.requestMatchers("/user/**").access(new WebExpressionAuthorizationManager("@webSecurity.check(authentication,request)"))
|
|
...
|
|
)
|
|
----
|
|
|
|
.XML
|
|
[source,xml,role="secondary"]
|
|
----
|
|
<http>
|
|
<intercept-url pattern="/user/**"
|
|
access="@webSecurity.check(authentication,request)"/>
|
|
...
|
|
</http>
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
http {
|
|
authorizeRequests {
|
|
authorize("/user/**", "@webSecurity.check(authentication,request)")
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[[el-access-web-path-variables]]
|
|
=== Path Variables in Web Security Expressions
|
|
|
|
At times, it is nice to be able to refer to path variables within a URL.
|
|
For example, consider a RESTful application that looks up a user by ID from a URL path in a format of `+/user/{userId}+`.
|
|
|
|
You can easily refer to the path variable by placing it in the pattern.
|
|
For example, you could use the following if you had a Bean with the name of `webSecurity` that contains the following method signature:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
public class WebSecurity {
|
|
public boolean checkUserId(Authentication authentication, int id) {
|
|
...
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
class WebSecurity {
|
|
fun checkUserId(authentication: Authentication?, id: Int): Boolean {
|
|
// ...
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
You could then refer to the method as follows:
|
|
|
|
.Path Variables
|
|
====
|
|
.Java
|
|
[source,java,role="primary",attrs="-attributes"]
|
|
----
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)"))
|
|
...
|
|
);
|
|
----
|
|
|
|
.XML
|
|
[source,xml,role="secondary",attrs="-attributes"]
|
|
----
|
|
<http>
|
|
<intercept-url pattern="/user/{userId}/**"
|
|
access="@webSecurity.checkUserId(authentication,#userId)"/>
|
|
...
|
|
</http>
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary",attrs="-attributes"]
|
|
----
|
|
http {
|
|
authorizeRequests {
|
|
authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)")
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
In this configuration, URLs that match would pass in the path variable (and convert it) into the `checkUserId` method.
|
|
For example, if the URL were `/user/123/resource`, the ID passed in would be `123`.
|
|
|
|
== Method Security Expressions
|
|
Method security is a bit more complicated than a simple allow or deny rule.
|
|
Spring Security 3.0 introduced some new annotations to allow comprehensive support for the use of expressions.
|
|
|
|
[[el-pre-post-annotations]]
|
|
=== @Pre and @Post Annotations
|
|
There are four annotations that support expression attributes to allow pre and post-invocation authorization checks and also to support filtering of submitted collection arguments or return values.
|
|
They are `@PreAuthorize`, `@PreFilter`, `@PostAuthorize`, and `@PostFilter`.
|
|
Their use is enabled through the `global-method-security` namespace element:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<global-method-security pre-post-annotations="enabled"/>
|
|
----
|
|
====
|
|
|
|
==== Access Control using @PreAuthorize and @PostAuthorize
|
|
The most obviously useful annotation is `@PreAuthorize`, which decides whether a method can actually be invoked or not.
|
|
The following example (from the {gh-samples-url}/servlet/xml/java/contacts["Contacts" sample application]) uses the `@PreAuthorize` annotation:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@PreAuthorize("hasRole('USER')")
|
|
public void create(Contact contact);
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@PreAuthorize("hasRole('USER')")
|
|
fun create(contact: Contact?)
|
|
----
|
|
====
|
|
|
|
This means that access is allowed only for users with the `ROLE_USER` role.
|
|
Obviously, the same thing could easily be achieved by using a traditional configuration and a simple configuration attribute for the required role.
|
|
However, consider the following example:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@PreAuthorize("hasPermission(#contact, 'admin')")
|
|
public void deletePermission(Contact contact, Sid recipient, Permission permission);
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@PreAuthorize("hasPermission(#contact, 'admin')")
|
|
fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?)
|
|
----
|
|
====
|
|
|
|
Here, we actually use a method argument as part of the expression to decide whether the current user has the `admin` permission for the given contact.
|
|
The built-in `hasPermission()` expression is linked into the Spring Security ACL module through the application context, as we <<el-permission-evaluator,see later in this section>>.
|
|
You can access any of the method arguments by name as expression variables.
|
|
|
|
Spring Security can resolve the method arguments in a number of ways.
|
|
Spring Security uses `DefaultSecurityParameterNameDiscoverer` to discover the parameter names.
|
|
By default, the following options are tried for a method.
|
|
|
|
* If Spring Security's `@P` annotation is present on a single argument to the method, the value is used.
|
|
This is useful for interfaces compiled with a JDK prior to JDK 8 (which do not contain any information about the parameter names).
|
|
The following example uses the `@P` annotation:
|
|
|
|
+
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
import org.springframework.security.access.method.P;
|
|
|
|
...
|
|
|
|
@PreAuthorize("#c.name == authentication.name")
|
|
public void doSomething(@P("c") Contact contact);
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
import org.springframework.security.access.method.P
|
|
|
|
...
|
|
|
|
@PreAuthorize("#c.name == authentication.name")
|
|
fun doSomething(@P("c") contact: Contact?)
|
|
----
|
|
====
|
|
|
|
+
|
|
|
|
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation.
|
|
|
|
* If Spring Data's `@Param` annotation is present on at least one parameter for the method, the value is used.
|
|
This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names.
|
|
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?
|
|
----
|
|
====
|
|
+
|
|
|
|
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation.
|
|
|
|
* If JDK 8 was used to compile the source with the `-parameters` argument and Spring 4+ is being used, the standard JDK reflection API is used to discover the parameter names.
|
|
This works on both classes and interfaces.
|
|
|
|
* Finally, if the code was compiled with the 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, annotations or the JDK 8 approach must be used.
|
|
|
|
.[[el-pre-post-annotations-spel]]
|
|
Any SpEL functionality is available within the expression, so you can also access properties on the arguments.
|
|
For example, if you wanted a particular method to allow access only to a user whose username matched that of the contact, you could write
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@PreAuthorize("#contact.name == authentication.name")
|
|
public void doSomething(Contact contact);
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@PreAuthorize("#contact.name == authentication.name")
|
|
fun doSomething(contact: Contact?)
|
|
----
|
|
====
|
|
|
|
.[[el-pre-post-annotations-post]]
|
|
Here, we access another built-in expression, `authentication`, which is the `Authentication` stored in the security context.
|
|
You can also access its `principal` property directly, by using the `principal` expression.
|
|
The value is often a `UserDetails` instance, so you might use an expression such as `principal.username` or `principal.enabled`.
|
|
|
|
==== Filtering using @PreFilter and @PostFilter
|
|
Spring Security supports filtering of collections, arrays, maps, and streams by using expressions.
|
|
This is most commonly performed on the return value of a method.
|
|
The following example uses `@PostFilter`:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@PreAuthorize("hasRole('USER')")
|
|
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
|
|
public List<Contact> getAll();
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@PreAuthorize("hasRole('USER')")
|
|
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
|
|
fun getAll(): List<Contact?>
|
|
----
|
|
====
|
|
|
|
When using the `@PostFilter` annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false.
|
|
For an array, a new array instance that contains filtered elements is returned.
|
|
`filterObject` refers to the current object in the collection.
|
|
When a map is used, it refers to the current `Map.Entry` object, which lets you use `filterObject.key` or `filterObject.value` in the expression.
|
|
You can also filter before the method call by using `@PreFilter`, though this is a less common requirement.
|
|
The syntax is the same. However, if there is more than one argument that is a collection type, you have to select one by name using the `filterTarget` property of this annotation.
|
|
|
|
Note that filtering is obviously not a substitute for tuning your data retrieval queries.
|
|
If you are filtering large collections and removing many of the entries, this is likely to be inefficient.
|
|
|
|
|
|
[[el-method-built-in]]
|
|
=== Built-In Expressions
|
|
There are some built-in expressions that are specific to method security, which we have already seen in use earlier.
|
|
The `filterTarget` and `returnValue` values are simple enough, but the use of the `hasPermission()` expression warrants a closer look.
|
|
|
|
|
|
[[el-permission-evaluator]]
|
|
==== The PermissionEvaluator interface
|
|
`hasPermission()` expressions are delegated to an instance of `PermissionEvaluator`.
|
|
It is intended to bridge between the expression system and Spring Security's ACL system, letting you specify authorization constraints on domain objects, based on abstract permissions.
|
|
It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required.
|
|
The interface has two methods:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
boolean hasPermission(Authentication authentication, Object targetDomainObject,
|
|
Object permission);
|
|
|
|
boolean hasPermission(Authentication authentication, Serializable targetId,
|
|
String targetType, Object permission);
|
|
----
|
|
====
|
|
|
|
These methods map directly to the available versions of the expression, with the exception that the first argument (the `Authentication` object) is not supplied.
|
|
The first is used in situations where the domain object, to which access is being controlled, is already loaded.
|
|
Then the expression returns `true` if the current user has the given permission for that object.
|
|
The second version is used in cases where the object is not loaded but its identifier is known.
|
|
An abstract "`type`" specifier for the domain object is also required, letting the correct ACL permissions be loaded.
|
|
This has traditionally been the Java class of the object but does not have to be, as long as it is consistent with how the permissions are loaded.
|
|
|
|
To use `hasPermission()` expressions, you have to explicitly configure a `PermissionEvaluator` in your application context.
|
|
The following example shows how to do so:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<security:global-method-security pre-post-annotations="enabled">
|
|
<security:expression-handler ref="expressionHandler"/>
|
|
</security:global-method-security>
|
|
|
|
<bean id="expressionHandler" class=
|
|
"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
|
|
<property name="permissionEvaluator" ref="myPermissionEvaluator"/>
|
|
</bean>
|
|
----
|
|
====
|
|
|
|
Where `myPermissionEvaluator` is the bean which implements `PermissionEvaluator`.
|
|
Usually, this is the implementation from the ACL module, which is called `AclPermissionEvaluator`.
|
|
See the {gh-samples-url}/servlet/xml/java/contacts[`Contacts`] sample application configuration for more details.
|
|
|
|
==== Method Security Meta Annotations
|
|
|
|
You can make use of meta annotations for method security to make your code more readable.
|
|
This is especially convenient if you find that you repeat the same complex expression throughout your code base.
|
|
For example, consider the following:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
@PreAuthorize("#contact.name == authentication.name")
|
|
----
|
|
====
|
|
|
|
Instead of repeating this everywhere, you can create a meta annotation:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@Retention(RetentionPolicy.RUNTIME)
|
|
@PreAuthorize("#contact.name == authentication.name")
|
|
public @interface ContactPermission {}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@Retention(AnnotationRetention.RUNTIME)
|
|
@PreAuthorize("#contact.name == authentication.name")
|
|
annotation class ContactPermission
|
|
----
|
|
====
|
|
|
|
You can use meta annotations for any of the Spring Security method security annotations.
|
|
To remain compliant with the specification, JSR-250 annotations do not support meta annotations.
|
|
|