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

Spring Security 3.0 介绍使用 Spring EL 表达式除了配置属性和访问决策的选民已见过的简单的使用授权机制的能力。基于访问控制的表达是建立在相同的结构,但允许复杂的 Boolean 逻辑被封装在一个单一的表达。

27.1 概述

Spring Security 使用 Spring EL 作为表达式支持,如果你有兴趣更深入地理解这个主题,那么应该看看它是如何工作的。表达式以 "root object" 作为评估上下文的一部分进行评估。Spring Security 使用用于 web 和方法安全性的特定类作为根对象,以便提供内置的表达式和对值(如当前主体)的访问。

27.1.1 常用内建表达式

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

表格 27.1. 常用内建表达式

表达式 描述

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')


27.2 Web 安全表达式

要使用表达式来保护单个 URL,首先需要将 <http> 元素中的 use-expressions 属性设置为 true。Spring Security 将预期 <intercept-url> 元素的 access 属性包含 Spring EL 表达式。表达式应该评估为 Boolean,定义是否应该允许访问。例如:

<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 中。因此,如果不使用命名空间,并且希望使用表达式,则必须将其中一个添加到配置中。

27.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)")
				...

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

有时,能够在 URL 内引用路径变量是很好的。例如,考虑一个 RESTful 应用程序,它以格式 /user/{userId} 从 URL 路径中按 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。

27.3 方法安全表达式

方法安全性比简单的允许或拒绝规则更复杂一些。Spring Security 3.0 引入了一些新的注释,以便允许对表达式的使用进行全面支持。

27.3.1 @Pre 和 @Post 注解

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

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

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

最明显的有用注解是 @PreAuthorize,它决定是否真的可以调用方法。例如(从 "Contacts" 示例应用程序)

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

这意味着只允许使用具有 "ROLE_USER" 角色的用户访问。显然,使用传统配置和所需角色的简单配置属性可以容易地实现相同的目的。但如何:

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

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

有许多方法可以在 Spring Security 中解决方法参数。Spring Security 使用 DefaultSecurityParameterNameDiscoverer 来发现参数名。默认情况下,为整个方法尝试以下选项。

  • 如果 Spring Security 的 @P 注解存在于该方法的单个参数上,则将使用该值。这对于 JDK 8 之前不包含关于参数名称的任何信息的 JDK 编译的接口是有用的。例如:

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

    在幕后,这个使用使用 AnnotationParameterNameDiscoverer 实现,它可以定制成支持任何指定注解的 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 实现,它可以定制成支持任何指定注解的 value 属性。

  • 如果使用 JDK 8 编译带有 -parameters 参数的源,并使用 Spring 4+,则使用标准 JDK 反射 API 来发现参数名称。这在两个类和接口上都有作用。
  • 最后,如果代码是用调试符号编译的,则使用调试符号发现参数名称。这将不适用于接口,因为它们没有关于参数名称的调试信息。对于接口,必须使用注解或 JDK 8 方法。

表达式中可以使用任何 Spring EL 功能,因此你也可以访问参数上的属性。例如,如果希望某个特定的方法只允许访问用户名与联系人的用户名匹配的用户,则可以

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

这里,我们正在访问另一个内置表达式,即 authentication,它是安全上下文中存储的 Authentication。还可以使用表达式 principal 直接访问其 "principal" 属性。该值通常是一个用户详细信息实例,因此您可以使用一个表达式,如 principal.username 或 principal.enabled.。

通常情况下,你可能希望在调用该方法之后执行访问控制检查。这可以使用 @PostAuthorize 注解来实现。若要从方法访问返回值,请使用表达式中的内置名称 returnObject。

使用 @PreFilter 和 @PostFilter 过滤

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

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

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

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

27.3.2 内置表达式

有一些内置的表达式是特定于方法安全性的,我们已经在上面使用过。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 对象)。第一个是在已经访问访问的域对象已经加载的情况下使用的。如果当前用户对该对象具有给定的权限,则表达式将返回 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 的 bean。通常,这将是 ACL 模块的实现,它被称为 AclPermissionEvaluator。有关的详细信息请参阅 "Contacts" 示例应用程序配置。

方法安全元注解

可以使用元注解来实现方法安全性,从而使代码更加可读。如果发现在整个代码库中重复相同的复杂表达式,这一点尤其方便。例如,考虑以下内容:

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

我们可以创建一个可以使用的元注解,而不是到处重复。

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

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