基于表达式的访问控制

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springboot.io - Spring Boot中文社区 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring Security 3.0引入了使用Spring表达式语言(SpEL)表达式作为授权机制的能力,此外还有现有的配置属性和访问决策投票人。基于表达式的访问控制建立在相同的架构上,但让复杂的布尔逻辑被封装在一个表达式中。

概述

Spring Security使用SpEL来支持表达式,如果你有兴趣更深入地了解这个话题,你应该看看它是如何工作的。表达式是以 "根对象" (“root object”)作为 evaluation context 的一部分进行 evaluat 的。Spring Security使用用于Web和方法安全的特定类作为根对象,以提供内置的表达式和对值的访问,如当前principal。

常见的内置表达式

表达式根对象的基类是 SecurityExpressionRoot。这提供了一些常见的表达式,在web和方法安全中都可以使用。

Table 1. Common built-in expressions
表达式 说明

hasRole(String role)

如果当前的principal 有指定的角色,则返回 true

例如: hasRole('admin')

默认情况下,如果提供的角色不是以 ROLE_ 开头,就会被添加。你可以通过修改 DefaultWebSecurityExpressionHandlerdefaultRolePrefix 来定制这一行为。

hasAnyRole(String…​ roles)

如果当前的 principal 有任何提供的角色(以逗号分隔的字符串列表形式给出),则返回 true

例如: hasAnyRole('admin', 'user').

默认情况下,如果提供的角色不是以 ROLE_ 开头,它就会被添加。你可以通过修改 DefaultWebSecurityExpressionHandlerdefaultRolePrefix 来定制这一行为。

hasAuthority(String authority)

如果当前 principal 有指定的权限,则返回 true

例如: hasAuthority('read')

hasAnyAuthority(String…​ authorities)

如果当前 principal 有任何提供的授权(以逗号分隔的字符串列表形式给出),则返回 true

例如: hasAnyAuthority('read', 'write').

principal

允许直接访问代表当前用户的 principal 对象。

authentication

允许直接访问从`SecurityContext`获得的当前`Authentication`对象。

permitAll

结果总是为 true

denyAll

结果总是为 false

isAnonymous()

如果当前 principal 是匿名用户,返回 true

isRememberMe()

如果当前 principal 是一个记住我(remember-me)的用户,返回`true`。

isAuthenticated()

如果用户不是匿名的,返回 true

isFullyAuthenticated()

如果该用户不是匿名用户,也不是remember-me用户,则返回 true

hasPermission(Object target, Object permission)

如果用户对所提供的目标有访问权限,返回 true。 例如, hasPermission(domainObject, 'read').

hasPermission(Object targetId, String targetType, Object permission)

如果用户对所提供的目标有访问权限,返回 true。 例如, hasPermission(1, 'com.example.domain.Message', 'read').

Web Security 的表达方式

要使用表达式来保护单个URL,你首先需要将 <http> 元素中的 use-expressions 属性设置为 true。然后 Spring Security 希望 <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 表达式是一个额外的内置表达式,专门用于web安全。它由 WebSecurityExpressionRoot 类定义,该类的一个实例在评估Web访问表达式时被用作表达式根对象。这个对象还直接暴露了名称为 requestHttpServletRequest 对象,这样你就可以在表达式中直接调用请求。如果正在使用表达式,一个 WebExpressionVoter 被添加到名字空间使用的 AccessDecisionManager 中。所以,如果你不使用命名空间而想使用表达式,你必须在你的配置中添加其中一个。

在Web Security表达式中引用Bean

如果你想扩展可用的表达式,你可以很容易地引用你所暴露的任何Spring Bean。例如,假设你有一个名字为 webSecurity 的Bean,包含以下方法签名,你可以使用下面的方法。

Java
public class WebSecurity {
		public boolean check(Authentication authentication, HttpServletRequest request) {
				...
		}
}
Kotlin
class WebSecurity {
    fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean {
        // ...
    }
}

然后你可以参考以下方法。

Example 1. Refer to method
Java
http
    .authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/user/**").access(new WebExpressionAuthorizationManager("@webSecurity.check(authentication,request)"))
        ...
    )
XML
<http>
	<intercept-url pattern="/user/**"
		access="@webSecurity.check(authentication,request)"/>
	...
</http>
Kotlin
http {
    authorizeRequests {
        authorize("/user/**", "@webSecurity.check(authentication,request)")
    }
}

Web Security表达式中的路径(Path )变量

有时,能够在URL中引用路径变量是件好事。例如,考虑一个RESTful应用程序,通过 /user/{userId} 格式的URL路径来查找用户的ID。

你可以通过把路径变量放在 pattern 中来轻松地引用它。例如,如果你有一个名字为 webSecurity 的Bean,包含以下方法签名,你可以使用下面的方法。

Java
public class WebSecurity {
		public boolean checkUserId(Authentication authentication, int id) {
				...
		}
}
Kotlin
class WebSecurity {
    fun checkUserId(authentication: Authentication?, id: Int): Boolean {
        // ...
    }
}

然后你可以参考以下方法。

Example 2. Path Variables
Java
http
	.authorizeHttpRequests(authorize -> authorize
		.requestMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
		...
	);
XML
<http>
	<intercept-url pattern="/user/{userId}/**"
		access="@webSecurity.checkUserId(authentication,#userId)"/>
	...
</http>
Kotlin
http {
    authorizeRequests {
        authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)")
    }
}

在这个配置中,匹配的URL将传入路径变量(并将其转换)到 checkUserId 方法中。例如,如果URL是 /user/123/resource,传入的ID将是 123

方法安全(Method Security )表达式

方法安全比简单的允许或拒绝规则要复杂一些。Spring Security 3.0引入了一些新的注解,允许全面支持表达式的使用。

@Pre 和 @Post 注解

有四个注解支持表达式属性,以允许授权前和授权后的检查,也支持对提交的集合参数或返回值进行过滤。它们是 @PreAuthorize, @PreFilter, @PostAuthorize, 和 @PostFilter。它们的使用是通过 global-method-security 命名空间元素启用的。

<global-method-security pre-post-annotations="enabled"/>

使用@PreAuthorize和@PostAuthorize进行访问控制

最明显有用的注解是 @PreAuthorize,它决定了一个方法是否真的可以被调用。下面的例子(来自 "Contacts"示例程序 )使用了 @PreAuthorize 注解。

Java
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
Kotlin
@PreAuthorize("hasRole('USER')")
fun create(contact: Contact?)

这意味着只允许具有 ROLE_USER 角色的用户访问。显然,同样的事情可以通过使用传统的配置和所需角色的简单配置属性来轻松实现。然而,考虑下面的例子。

Java
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
Kotlin
@PreAuthorize("hasPermission(#contact, 'admin')")
fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?)

在这里,我们实际上使用了一个方法参数作为表达式的一部分,来决定当前用户是否有给定 contact 的 admin 权限。内置的 hasPermission() 表达式通过应用程序上下文链接到Spring Security ACL模块,正如我们在 本节后面 看到的那样。你可以以表达式中的变量名访问任何方法参数。

Spring Security可以通过多种方式解析方法参数。Spring Security使用 DefaultSecurityParameterNameDiscoverer 来发现参数名称。默认情况下,对于一个方法会尝试以下选项。

  • 如果Spring Security的 @P 注解出现在方法的单个参数上,则会使用该值。这对于用JDK 8之前的JDK编译的接口很有用(这些接口不包含任何关于参数名的信息)。下面的例子使用了 @P 注解。

    Java
    import org.springframework.security.access.method.P;
    
    ...
    
    @PreAuthorize("#c.name == authentication.name")
    public void doSomething(@P("c") Contact contact);
    Kotlin
    import org.springframework.security.access.method.P
    
    ...
    
    @PreAuthorize("#c.name == authentication.name")
    fun doSomething(@P("c") contact: Contact?)

    在幕后,这是通过使用 AnnotationParameterNameDiscoverer 实现的,你可以定制它以支持任何指定注解的值属性。

  • 如果Spring Data的 @Param 注解存在于方法的至少一个参数上,那么就会使用该值。这对于用JDK 8之前的JDK编译的接口很有用,因为这些接口不包含任何关于参数名称的信息。下面的例子使用了 @Param 注解。

    Java
    import org.springframework.data.repository.query.Param;
    
    ...
    
    @PreAuthorize("#n == authentication.name")
    Contact findContactByName(@Param("n") String name);
    Kotlin
    import org.springframework.data.repository.query.Param
    
    ...
    
    @PreAuthorize("#n == authentication.name")
    fun findContactByName(@Param("n") name: String?): Contact?

    在幕后,这是通过使用 AnnotationParameterNameDiscoverer 实现的,你可以定制它以支持任何指定注解的值属性。

  • 如果JDK 8被用来编译带有 -parameters 参数的源码,而Spring 4+正在被使用,那么标准的JDK反射API被用来发现参数名称。这对类和接口都有效。

  • 最后,如果代码是用调试符号编译的,那么参数名就可以通过调试符号来发现。这对接口不起作用,因为它们没有关于参数名称的调试信息。对于接口,必须使用注解或JDK 8的方法。

任何SpEL功能都可以在表达式中使用,所以你也可以访问参数上的属性。例如,如果你想让一个特定的方法只允许与联系人的用户名相符的用户访问,你可以写道

Java
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
Kotlin
@PreAuthorize("#contact.name == authentication.name")
fun doSomething(contact: Contact?)

这里我们要访问另一个内置表达式,authentication,它是存储在安全上下文中的 Authentication。你也可以使用表达式 principal,直接访问它的 "principal" 属性。这个值通常是一个 UserDetails 实例,所以你可以使用 principal.usernameprincipal.enabled 这样的表达式。

在这里,我们访问另一个内置表达式,authentication,它是存储在 security context 中的 Authentication。你也可以通过使用 principal 表达式直接访问其 principal 属性。这个值通常是一个 UserDetails 实例,所以你可以使用 principal.usernameprincipal.enabled 这样的表达式。

使用@PreFilter和@PostFilter进行过滤

Spring Security支持通过使用表达式对集合、数组、Map和Stream进行过滤。这通常是在方法的返回值上进行的。下面的例子使用了 @PostFilter

Java
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();
Kotlin
@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.keyfilterObject.value。你也可以通过使用 @PreFilter 在方法调用前进行过滤,尽管这是不太常见的要求。语法是一样的。然而,如果有一个以上的参数是集合类型,你必须使用该注解的 filterTarget 属性按名称选择一个。

请注意,过滤显然不能替代对数据检索查询的调整。如果你正在过滤大的集合并删除许多条目,这很可能是低效的。

内置的表达式

有一些内置的表达式是专门针对方法安全的,我们已经在前面看到了这些表达式的使用。filterTargetreturnValue 值很简单,但 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 对象)。第一个参数用于被控制访问的域对象已经被加载的情况。如果当前用户对该对象有给定的权限,那么表达式就会返回 true。第二个版本用于对象未被加载但其标识符已知的情况。领域对象的一个抽象的 "类型" 指定器也是需要的,让正确的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 的bean。通常,这是ACL模块的实现,它被称为 AclPermissionEvaluator。更多的细节请看 Contacts 示例应用程序的配置。

方法安全元注解

你可以利用方法安全的元注释来使你的代码更加可读。如果你发现你在整个代码库中重复相同的复杂表达式,这就特别方便。例如,考虑下面的情况。

@PreAuthorize("#contact.name == authentication.name")

你可以创建一个元注解,而不是到处重复这个,你可以创建一个元注解。

Java
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
Kotlin
@Retention(AnnotationRetention.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
annotation class ContactPermission

你可以为任何Spring Security方法的安全注解使用元注解。为了与规范保持一致,JSR-250注解不支持元注解。