26. 基于表达式的访问控制

Spring Security 3.0 引入了使用 Spring EL 表达式作为授权机制的功能,此外还可以简单地使用配置属性和访问决定投票器。基于表达式的访问控制基于相同的体系结构,但是允许将复杂的布尔逻辑封装在单个表达式中。

26.1 Overview

Spring Security 使用 Spring EL 来支持表达,如果您想更深入地了解该主题,则应该看看它的工作方式。使用“根对象”评估表达式作为评估上下文的一部分。 Spring Security 使用特定的类作为 Web 对象和方法的安全性作为根对象,以提供内置的表达式并访问诸如当前主体的值。

26.1.1 常见的内置表达式

表达式根对象的 Base Class 为SecurityExpressionRoot。这提供了 Web 和方法安全性中都可用的一些常用表达式。

表 26.1 常见的内置表达式

ExpressionDescription
hasRole([role])如果当前主体具有指定角色,则返回true。默认情况下,如果提供的角色不是以“ ROLE_”开头,则会添加该角色。可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。
hasAnyRole([role1,role2])如果当前主体具有提供的任何角色(以逗号分隔的字符串列表形式),则返回true。默认情况下,如果提供的角色不是以“ ROLE_”开头,则会添加该角色。可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。
hasAuthority([authority])如果当前主体具有指定的权限,则返回true
hasAnyAuthority([authority1,authority2])如果当前委托人具有提供的任何角色(以逗号分隔的字符串列表形式),则返回true
principal允许直接访问代表当前用户的主体对象
authentication允许直接访问从SecurityContext获取的当前Authentication对象
permitAll始终计算为true
denyAll始终计算为false
isAnonymous()如果当前主体是匿名用户,则返回true
isRememberMe()如果当前主体是“记住我”用户,则返回true
isAuthenticated()如果用户不是匿名用户,则返回true
isFullyAuthenticated()如果用户不是匿名用户或“记住我”用户,则返回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')

26.2 Web 安全表达式

要使用表达式保护单个 URL,首先需要将<http>元素中的use-expressions属性设置为true。然后,Spring Security 将期望<intercept-url>元素的access属性包含 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 访问表达式时,该实例的一个实例用作表达式根对象。该对象还直接以名称request公开HttpServletRequest对象,因此您可以直接在表达式中调用请求。如果使用表达式,则将WebExpressionVoter添加到命名空间使用的AccessDecisionManager中。因此,如果您不使用名称空间而要使用表达式,则必须将其中之一添加到配置中。

26.2.1 在 Web 安全表达式中引用 Bean

如果您希望扩展可用的表达式,则可以轻松地引用您公开的任何 Spring Bean。例如,假设您有一个名称为webSecurity的 Bean,其中包含以下方法签名:

public class WebSecurity {
		public boolean check(Authentication authentication, HttpServletRequest request) {
				...
		}
}

您可以使用以下方法引用该方法:

<http>
	<intercept-url pattern="/user/**"
		access="@webSecurity.check(authentication,request)"/>
	...
</http>

或在 Java 配置中

http
		.authorizeRequests()
				.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
				...

26.2.2 Web 安全表达式中的路径变量

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

您可以通过将路径变量放在模式中来轻松地引用它。例如,如果您有一个名称为webSecurity的 Bean,其中包含以下方法签名:

public class WebSecurity {
		public boolean checkUserId(Authentication authentication, int id) {
				...
		}
}

您可以使用以下方法引用该方法:

<http>
	<intercept-url pattern="/user/{userId}/**"
		access="@webSecurity.checkUserId(authentication,#userId)"/>
	...
</http>

或在 Java 配置中

http
		.authorizeRequests()
				.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
				...

在这两种配置中,匹配的 URL 会将路径变量传递(并将其转换)为 checkUserId 方法。例如,如果 URL 为/user/123/resource,则传入的 ID 为123

26.3 方法安全性表达式

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

26.3.1 @Pre 和@Post 注解

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

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

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

最明显有用的 Comments 是@PreAuthorize,它决定是否可以实际调用方法。例如(来自“联系人”示例应用程序)

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

这意味着只允许角色为“ ROLE_USER”的用户访问。显然,使用传统配置和所需角色的简单配置属性可以轻松实现同一目标。但是关于:

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

在这里,我们实际上使用方法参数作为表达式的一部分,以确定当前用户是否具有给定联系人的“Management 员”权限。内置的hasPermission()表达式通过应用程序上下文链接到 Spring Security ACL 模块,就像我们将see below一样。您可以按名称作为表达式变量访问任何方法参数。

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

  • 如果 Spring Security 的@PComments 出现在方法的单个参数上,则将使用该值。这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,该接口不包含有关参数名称的任何信息。例如:
import org.springframework.security.access.method.P;

...

@PreAuthorize("#c.name == authentication.name")
public void doSomething(@P("c") Contact contact);

在后台使用AnnotationParameterNameDiscoverer实现此用法,可以对其进行自定义以支持任何指定 Comments 的 value 属性。

  • 如果该方法的至少一个参数上存在 Spring Data 的@Param注解,则将使用该值。这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,该接口不包含有关参数名称的任何信息。例如:
import org.springframework.data.repository.query.Param;

...

@PreAuthorize("#n == authentication.name")
Contact findContactByName(@Param("n") String name);

在后台使用AnnotationParameterNameDiscoverer实现此用法,可以对其进行自定义以支持任何指定 Comments 的 value 属性。

  • 如果使用 JDK 8 通过-parameters 参数编译源,并且使用 Spring 4,则使用标准 JDK 反射 API 来发现参数名称。这适用于类和接口。

  • 最后,如果代码是使用调试符号编译的,则将使用调试符号发现参数名称。这对于接口不起作用,因为它们没有有关参数名称的调试信息。对于接口,必须使用 Comments 或 JDK 8 方法。

表达式中提供了任何 Spring-EL 功能,因此您也可以访问参数的属性。例如,如果您希望一种特定的方法仅允许访问其用户名与联系人的用户名匹配的用户,则可以编写

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);

在这里,我们正在访问另一个内置表达式authentication,它是存储在安全上下文中的Authentication。您也可以使用表达式principal直接访问其“主要”属性。该值通常是UserDetails实例,因此您可以使用principal.usernameprincipal.enabled之类的表达式。

不太常见的是,您可能希望在调用该方法之后执行访问控制检查。这可以使用@PostAuthorizeComments 来实现。要从方法访问返回值,请在表达式中使用内置名称returnObject

使用@PreFilter 和@PostFilter 进行过滤

您可能已经知道,Spring Security 支持集合和数组的过滤,现在可以使用表达式来实现。这通常在方法的返回值上执行。例如:

@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();

当使用@PostFilterComments 时,Spring Security 遍历返回的集合并删除提供的表达式为 false 的所有元素。名称filterObject表示集合中的当前对象。您也可以使用@PreFilter进行方法调用之前的过滤,尽管这种要求不太常见。语法是一样的,但是如果有多个参数是集合类型,则必须使用此注解的filterTarget属性通过名称选择一个。

请注意,过滤显然不能代替调整数据检索查询。如果要过滤大型集合并删除许多条目,则效率可能很低。

26.3.2 内置表达式

有一些特定于方法安全性的内置表达式,我们已经在上面使用过。 filterTargetreturnValue值很简单,但是使用hasPermission()表达式需要仔细观察。

PermissionEvaluator 界面

hasPermission()表达式委托给PermissionEvaluator的实例。它旨在在表达式系统和 Spring Security 的 ACL 系统之间架起 bridge 梁,使您可以基于抽象权限在域对象上指定授权约束。它对 ACL 模块没有明确的依赖关系,因此如果需要,您可以将其换成其他实现。该接口有两种方法:

boolean hasPermission(Authentication authentication, Object targetDomainObject,
							Object permission);

boolean hasPermission(Authentication authentication, Serializable targetId,
							String targetType, Object permission);

除了未提供第一个参数(Authentication对象)之外,该参数直接 Map 到表达式的可用版本。第一种方法用于已经控制了访问权限的域对象已加载的情况。如果当前用户对该对象具有给定的权限,则 expression 将返回 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。通常,这将是来自称为AclPermissionEvaluator的 ACL 模块的实现。有关更多详细信息,请参见“联系人”示例应用程序配置。

方法安全性元 Comments

您可以使用元 Comments 来保证方法的安全性,以使代码更具可读性。如果发现在整个代码库中重复相同的复杂表达式,这将特别方便。例如,考虑以下内容:

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

不必在所有地方重复此操作,我们可以创建一个可以用作替代的元 Comments。

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}

元 Comments 可以用于任何 Spring Security 方法安全 Comments。为了保持符合规范,JSR-250 注解不支持元注解。