对于最新的稳定版本,请使用 Spring Security 6.4.3! |
基于表达式的访问控制
概述
Spring Security 使用 Spring EL 来支持表达式,如果您有兴趣更深入地了解该主题,您应该了解它是如何工作的。 表达式使用“根对象”作为计算上下文的一部分进行计算。 Spring Security 使用特定的 Web 类和方法安全性作为根对象,以便提供内置表达式和对值(例如当前主体)的访问。
常见的内置表达式
表达式根对象的基类是SecurityExpressionRoot
.
这提供了一些在 Web 和方法安全性中都可用的常用表达式。
表达 | 描述 |
---|---|
|
返回 例如 默认情况下,如果提供的角色不以 'ROLE_' 开头,则会添加它。
这可以通过修改 |
|
返回 例如 默认情况下,如果提供的角色不以 'ROLE_' 开头,则会添加它。
这可以通过修改 |
|
返回 例如 |
|
返回 例如 |
|
允许直接访问表示当前用户的主体对象 |
|
允许直接访问当前的 |
|
Always 的计算结果为 |
|
Always 的计算结果为 |
|
返回 |
|
返回 |
|
返回 |
|
返回 |
|
返回 |
|
返回 |
Web 安全表达式
要使用表达式来保护单个 URL,您首先需要将use-expressions
属性中的<http>
元素设置为true
.
然后,Spring Security 将期望access
的属性<intercept-url>
元素来包含 Spring EL 表达式。
表达式的计算结果应为布尔值,定义是否应允许访问。
例如:
<http>
<intercept-url pattern="/admin*"
access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
...
</http>
在这里,我们定义了应用程序的 “admin” 区域(由 URL 模式定义)应该只对具有被授予权限 “admin” 且其 IP 地址与本地子网匹配的用户可用。
我们已经看到了内置的hasRole
表达式。
表达式hasIpAddress
是特定于 Web 安全性的附加内置表达式。
它由WebSecurityExpressionRoot
类,在计算 Web 访问表达式时,其实例用作表达式根对象。
这个对象还直接暴露了HttpServletRequest
名称下的 objectrequest
,因此您可以直接在表达式中调用请求。
如果正在使用表达式,则WebExpressionVoter
将添加到AccessDecisionManager
,该名称空间使用。
因此,如果您不使用命名空间并希望使用表达式,则必须将其中一个表达式添加到您的配置中。
在 Web 安全表达式中引用 Bean
如果你希望扩展可用的表达式,你可以很容易地引用你公开的任何 Spring Bean。
例如,假设您有一个名称为webSecurity
,其中包含以下方法签名:
-
Java
-
Kotlin
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}
class WebSecurity {
fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean {
// ...
}
}
您可以使用以下方法引用该方法:
-
Java
-
XML
-
Kotlin
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)
<http>
<intercept-url pattern="/user/**"
access="@webSecurity.check(authentication,request)"/>
...
</http>
http {
authorizeRequests {
authorize("/user/**", "@webSecurity.check(authentication,request)")
}
}
Web 安全表达式中的路径变量
有时,能够在 URL 中引用路径变量是件好事。
例如,假设一个 RESTful 应用程序从 URL 路径中按 id 查找用户,其格式为/user/{userId}
.
您可以通过将 path 变量放置在 pattern 中来轻松引用它。
例如,如果你有一个名称为webSecurity
,其中包含以下方法签名:
-
Java
-
Kotlin
public class WebSecurity {
public boolean checkUserId(Authentication authentication, int id) {
...
}
}
class WebSecurity {
fun checkUserId(authentication: Authentication?, id: Int): Boolean {
// ...
}
}
您可以使用以下方法引用该方法:
-
Java
-
XML
-
Kotlin
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
<http>
<intercept-url pattern="/user/{userId}/**"
access="@webSecurity.checkUserId(authentication,#userId)"/>
...
</http>
http {
authorizeRequests {
authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)")
}
}
在此配置中,匹配的 URL 会将 path 变量传入(并将其转换)到 checkUserId 方法中。
例如,如果 URL 是/user/123/resource
,则传入的 ID 将为123
.
方法安全表达式
方法安全性比简单的允许或拒绝规则要复杂一些。 Spring Security 3.0 引入了一些新的 Comments,以便全面支持表达式的使用。
@Pre 和 @Post 注释
有四个注释支持 expression 属性,以允许调用前和调用后授权检查,还支持筛选提交的集合参数或返回值。
他们是@PreAuthorize
,@PreFilter
,@PostAuthorize
和@PostFilter
.
它们的使用是通过global-method-security
namespace 元素:
<global-method-security pre-post-annotations="enabled"/>
使用 @PreAuthorize 和 @PostAuthorize 进行访问控制
最明显有用的注释是@PreAuthorize
它决定了方法是否真的可以被调用。
例如(来自 Contacts 示例应用程序)
-
Java
-
Kotlin
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
@PreAuthorize("hasRole('USER')")
fun create(contact: Contact?)
这意味着仅允许具有“ROLE_USER”角色的用户访问。 显然,使用传统配置和所需角色的简单配置属性可以很容易地实现相同的作。 但是呢:
-
Java
-
Kotlin
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
@PreAuthorize("hasPermission(#contact, 'admin')")
fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?)
在这里,我们实际上使用了一个 method 参数作为表达式的一部分,以确定当前用户是否具有给定联系人的 “admin” 权限。
内置的hasPermission()
expression 通过应用程序上下文链接到 Spring Security ACL 模块,我们将在下面看到。
您可以按名称作为表达式变量访问任何方法参数。
Spring Security 可以通过多种方式解析方法参数。
Spring Security 使用DefaultSecurityParameterNameDiscoverer
以发现参数名称。
默认情况下,将对整个方法尝试以下选项。
-
如果 Spring Security 的
@P
annotation 存在于该方法的单个参数上,则将使用该值。 这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,这些接口不包含有关参数名称的任何信息。 例如:-
Java
-
Kotlin
import org.springframework.security.access.method.P; ... @PreAuthorize("#c.name == authentication.name") public void doSomething(@P("c") Contact contact);
import org.springframework.security.access.method.P ... @PreAuthorize("#c.name == authentication.name") fun doSomething(@P("c") contact: Contact?)
Behind the scenes this is implemented using
AnnotationParameterNameDiscoverer
which can be customized 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 will be used. This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names. For example:-
Java
-
Kotlin
import org.springframework.data.repository.query.Param; ... @PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name);
import org.springframework.data.repository.query.Param ... @PreAuthorize("#n == authentication.name") fun findContactByName(@Param("n") name: String?): Contact?
Behind the scenes this is implemented using
AnnotationParameterNameDiscoverer
which can be customized 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, then the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces.
-
Last, if the code was compiled with the debug symbols, the parameter names will be discovered using the debug symbols. This will 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.
Any Spring-EL functionality is available within the expression, so you can also access properties on the arguments.
For example, if you wanted a particular method to only allow access to a user whose username matched that of the contact, you could write
-
Java
-
Kotlin
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
@PreAuthorize("#contact.name == authentication.name")
fun doSomething(contact: Contact?)
Here we are accessing another built-in expression, authentication
, which is the Authentication
stored in the security context.
You can also access its "principal" property directly, using the expression principal
.
The value will often be a UserDetails
instance, so you might use an expression like principal.username
or principal.enabled
.
Filtering using @PreFilter and @PostFilter
Spring Security supports filtering of collections, arrays, maps and streams using expressions.
This is most commonly performed on the return value of a method.
For example:
-
Java
-
Kotlin
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();
@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 will be returned containing filtered elements.
The name filterObject
refers to the current object in the collection.
In case when a map is used it will refer to the current Map.Entry
object which allows one to use filterObject.key
or filterObject.value
in the expresion.
You can also filter before the method call, using @PreFilter
, though this is a less common requirement.
The syntax is just the same, but if there is more than one argument which is a collection type then 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 then this is likely to be inefficient.
Built-In Expressions
There are some built-in expressions which are specific to method security, which we have already seen in use above.
The filterTarget
and returnValue
values are simple enough, but the use of the hasPermission()
expression warrants a closer look.
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, allowing you to 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:
boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission);
which 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 expression will return 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, allowing the correct ACL permissions to 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.
This would look something like this:
<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 will be the implementation from the ACL module which is called AclPermissionEvaluator
.
See the 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 are repeating the same complex expression throughout your code base.
For example, consider the following:
@PreAuthorize("#contact.name == authentication.name")
Instead of repeating this everywhere, we can create a meta annotation that can be used instead.
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
@Retention(AnnotationRetention.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
annotation class ContactPermission
Meta annotations can be used for any of the Spring Security method security annotations.
In order to remain compliant with the specification JSR-250 annotations do not support meta annotations.