对于最新的稳定版本,请使用 Spring Security 6.5.0! |
基于表达式的访问控制
概述
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?)
在幕后,这是通过使用
AnnotationParameterNameDiscoverer
,您可以自定义该属性以支持任何指定注释的 value 属性。 -
-
如果 Spring Data 的
@Param
annotation 的 Comments 存在于该方法的至少一个参数上,则使用该值。 这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,这些接口不包含有关参数名称的任何信息。 以下示例使用@Param
注解:-
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?
在幕后,这是通过使用
AnnotationParameterNameDiscoverer
,您可以自定义该属性以支持任何指定注释的 value 属性。 -
-
如果使用 JDK 8 编译源代码,并且
-parameters
参数和 Spring 4+ 时,使用标准的 JDK 反射 API 来发现参数名称。 这适用于类和接口。 -
最后,如果代码是使用调试符号编译的,则使用调试符号发现参数名称。 这不适用于接口,因为它们没有有关参数名称的调试信息。 对于接口,必须使用 Comments 或 JDK 8 方法。
-
Java
-
Kotlin
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
@PreAuthorize("#contact.name == authentication.name")
fun doSomething(contact: Contact?)
使用 @PreFilter 和 @PostFilter 进行筛选
Spring Security 支持使用表达式过滤集合、数组、映射和流。
这通常对方法的返回值执行。
以下示例使用@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?>
使用@PostFilter
注释时,Spring Security 会遍历返回的集合或 map 并删除提供的表达式为 false 的任何元素。
对于数组,将返回包含筛选元素的新数组实例。filterObject
引用集合中的当前对象。
当使用 map 时,它引用当前的Map.Entry
对象,它允许您使用filterObject.key
或filterObject.value
在表达式中。
您还可以在方法调用之前使用@PreFilter
,尽管这是一个不太常见的要求。
语法是相同的。但是,如果有多个参数是集合类型,则必须使用filterTarget
属性。
请注意,筛选显然不能替代优化数据检索查询。 如果要筛选大型集合并删除许多条目,则效率可能较低。
内置表达式
有一些特定于方法安全性的内置表达式,我们之前已经看到过这些表达式的使用。
这filterTarget
和returnValue
值很简单,但是使用hasPermission()
表达值得仔细观察。
PermissionEvaluator 接口
hasPermission()
表达式被委托给PermissionEvaluator
.
它旨在桥接表达式系统和 Spring Security 的 ACL 系统,让您根据抽象权限指定对域对象的授权约束。
它对 ACL 模块没有明确的依赖关系,因此如果需要,您可以将其换成替代实现。
该接口有两种方法:
boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission);
这些方法直接映射到表达式的可用版本,但第一个参数(Authentication
object) 的
第一种用于已加载 domain 对象(其访问权限被控制)的情况。
然后表达式返回true
当前用户是否具有该对象的给定权限。
第二个版本用于对象未加载但其标识符已知的情况。
还需要域对象的抽象 “type” 说明符,以便加载正确的 ACL 权限。
传统上,这是对象的 Java 类,但只要它与权限的加载方式一致,就不必如此。
要使用hasPermission()
表达式中,您必须显式配置PermissionEvaluator
在您的应用程序上下文中。
以下示例显示了如何执行此作:
<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>
哪里myPermissionEvaluator
是实现PermissionEvaluator
.
通常,这是 ACL 模块的实现,称为AclPermissionEvaluator
.
请参阅Contacts
示例应用程序配置,了解更多详细信息。
方法安全元注释
您可以利用元注释来实现方法安全性,以使您的代码更具可读性。 如果您发现在整个代码库中重复相同的复杂表达式,这将特别方便。 例如,请考虑以下情况:
@PreAuthorize("#contact.name == authentication.name")
你可以创建一个 meta 注解,而不是到处重复此作:
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
@Retention(AnnotationRetention.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
annotation class ContactPermission
你可以将元 Comments 用于任何 Spring Security 方法安全 Comments。 为了保持符合规范,JSR-250 注释不支持元注释。