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.
525 lines
18 KiB
525 lines
18 KiB
|
|
[[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. |
|
|
|
|