9 changed files with 263 additions and 575 deletions
@ -1,525 +0,0 @@
@@ -1,525 +0,0 @@
|
||||
|
||||
[[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. |
||||
|
||||
Loading…
Reference in new issue