123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- [[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.
|