对于最新的稳定版本,请使用 Spring Security 6.4.3! |
基于表达式的访问控制
概述
Spring Security 使用 SpEL 来支持表达式,如果您有兴趣更深入地了解该主题,则应了解其工作原理。 表达式使用“根对象”作为计算上下文的一部分进行计算。 Spring Security 使用特定的 Web 类和方法安全性作为根对象,以提供内置表达式和对值(例如当前主体)的访问。
常见的内置表达式
表达式根对象的基类是SecurityExpressionRoot
.
这提供了一些在 Web 和方法安全性中都可用的常用表达式:
表达 | 描述 |
---|---|
|
返回 例: 默认情况下,如果提供的角色不以 |
|
返回 例: 默认情况下,如果提供的角色不以 |
|
返回 例: |
|
返回 例: |
|
允许直接访问表示当前用户的主体对象。 |
|
允许直接访问当前的 |
|
Always 的计算结果为 |
|
Always 的计算结果为 |
|
返回 |
|
返回 |
|
返回 |
|
返回 |
|
返回 |
|
返回 |
Web 安全表达式
要使用表达式来保护单个 URL,您首先需要将use-expressions
属性中的<http>
元素设置为true
.
然后,Spring Security 需要access
的属性<intercept-url>
元素来包含 SPEL 表达式。
每个表达式的计算结果应为布尔值,定义是否应允许访问。
下面的清单显示了一个示例:
<http>
<intercept-url pattern="/admin*"
access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
...
</http>
在这里,我们定义了admin
区域(由 URL 模式定义)应仅对具有被授予权限 (admin
) 及其 IP 地址与本地子网匹配。
我们已经看到了内置的hasRole
表达式。
这hasIpAddress
expression 是特定于 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
.requestMatchers("/user/**").access(new WebExpressionAuthorizationManager("@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
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@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 注释
有四个注释支持表达式属性,以允许调用前和调用后授权检查,还支持筛选提交的集合参数或返回值。
他们是@PreAuthorize
,@PreFilter
,@PostAuthorize
和@PostFilter
.
它们的使用是通过global-method-security
namespace 元素:
<global-method-security pre-post-annotations="enabled"/>
使用 @PreAuthorize 和 @PostAuthorize 进行访问控制
最明显有用的注释是@PreAuthorize
,它决定方法是否真的可以调用。
以下示例(来自 “Contacts” 示例应用程序)使用@PreAuthorize
注解:
-
Java
-
Kotlin
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
@PreAuthorize("hasRole('USER')")
fun create(contact: Contact?)
这意味着仅允许具有ROLE_USER
角色。
显然,通过使用 required 角色的传统配置和简单的配置属性,可以很容易地实现相同的目的。
但是,请考虑以下示例:
-
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?)
在这里,我们实际上使用方法参数作为表达式的一部分来决定当前用户是否具有admin
给定联系人的权限。
内置的hasPermission()
expression通过应用程序上下文链接到 Spring Security ACL 模块,正如我们在本节后面看到的那样。
您可以按名称作为表达式变量访问任何方法参数。
Spring Security 可以通过多种方式解析方法参数。
Spring Security 使用DefaultSecurityParameterNameDiscoverer
以发现参数名称。
默认情况下,对方法尝试以下选项。
-
如果 Spring Security 的
@P
annotation 存在于方法的单个参数上,则使用该值。 这对于使用 JDK 8 之前的 JDK 编译的接口(不包含有关参数名称的任何信息)非常有用。 以下示例使用@P
注解:-
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 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
-
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 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.
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
-
Kotlin
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
@PreAuthorize("#contact.name == authentication.name")
fun doSomething(contact: Contact?)
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
-
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 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.
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.
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:
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:
<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 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:
@PreAuthorize("#contact.name == authentication.name")
Instead of repeating this everywhere, you can create a meta annotation:
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
@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.