# 基于表达式的访问控制

# 概述

Spring 安全性使用 Spring EL 作为表达式支持,如果你有兴趣更深入地理解该主题,那么你应该研究它是如何工作的。表达式使用“根对象”作为求值上下文的一部分进行求值。 Spring 安全性使用用于 Web 和方法安全性的特定类作为根对象,以便提供内置的表达式和对诸如当前主体的值的访问。

# 常见的内置表达式

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

Expression 说明
hasRole(String role) 返回true如果当前主体具有指定的角色。

例如,hasRole('admin')

默认情况下,如果提供的角色不是以“role_”开头,则将被添加。
这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix来定制。
hasAnyRole(String…​ roles) 返回true如果当前主体有任何提供的角色(以逗号分隔的字符串列表给出)。

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

默认情况下,如果提供的角色不是以“role_”开头,则将添加它。
这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix来定制。
hasAuthority(String authority) 如果当前主体具有指定的权限,则返回true

例如,hasAuthority('read')
hasAnyAuthority(String…​ authorities) 返回true如果当前主体有任何提供的权限(以逗号分隔的字符串列表给出)

例如,hasAnyAuthority('read', 'write')
principal 允许直接访问表示当前用户的主体对象
authentication 允许直接访问从SecurityContext中获得的当前Authentication对象
permitAll 总是计算为true
denyAll 总是计算为false
isAnonymous() 如果当前主体是匿名用户,则返回true
isRememberMe() 如果当前主体是 rememe-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 安全表达式

要使用表达式来保护单个 URL,你首先需要将<http>元素中的use-expressions属性设置为true。 Spring 然后,安全性将期望access元素的<intercept-url>属性包含 Spring EL 表达式。表达式应该计算为布尔值,定义是否应该允许访问。例如:

<http>
	<intercept-url pattern="/admin*"
		access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
	...
</http>

在这里,我们定义了应用程序的“管理”区域(由 URL 模式定义)应该仅对具有授权“管理”且其 IP 地址与本地子网匹配的用户可用。在前面的部分中,我们已经看到了内置的hasRole表达式。表达式hasIpAddress是一个特定于 Web 安全的附加内置表达式。它是由WebSecurityExpressionRoot类定义的,当计算 Web-Access 表达式时,它的一个实例被用作表达式的根对象。该对象还直接公开了名为requestHttpServletRequest对象,因此你可以在表达式中直接调用该请求。如果正在使用表达式,则将把WebExpressionVoter添加到名称空间使用的AccessDecisionManager中。因此,如果你 AREN 不使用名称空间,而希望使用表达式,则必须将其中的一个添加到配置中。

# 在 Web 安全表达式中引用 bean

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

爪哇

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

Kotlin

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

你可以使用以下方法参考该方法:

例 1。请参阅方法

爪哇

http
    .authorizeHttpRequests(authorize -> authorize
        .antMatchers("/user/**").access("@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 安全表达式中的路径变量

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

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

爪哇

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

Kotlin

class WebSecurity {
    fun checkUserId(authentication: Authentication?, id: Int): Boolean {
        // ...
    }
}

你可以使用以下方法参考该方法:

例 2。路径变量

爪哇

http
	.authorizeHttpRequests(authorize -> authorize
		.antMatchers("/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 将传入 PATH 变量(并将其转换为 checkuserid 方法)。例如,如果 URL 是/user/123/resource,那么传入的 ID 将是123

# 方法安全表达式

方法安全性比简单的允许或拒绝规则要复杂一些。 Spring Security3.0 引入了一些新的注释,以便允许对表达式的使用提供全面的支持。

# @pre 和 @post 注释

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

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

# 使用 @preauthorize 和 @postauthorize 的访问控制

最明显有用的注释是@PreAuthorize,它决定是否可以实际调用方法。例如(来自Contacts (opens new window)示例应用程序)

爪哇

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

Kotlin

@PreAuthorize("hasRole('USER')")
fun create(contact: Contact?)

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

爪哇

@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?)

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

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

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

    爪哇

    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实现的,它可以自定义以支持任何指定注释的 value 属性。

  • 如果在该方法的至少一个参数上存在 Spring 数据的@Param注释,则将使用该值。这对于在 JDK8 之前用 JDK 编译的接口非常有用,因为 JDK8 之前的接口不包含任何有关参数名称的信息。例如:

    爪哇

    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实现的,它可以自定义以支持任何指定注释的值属性。

  • 如果使用 JDK8 用-parameters 参数编译源代码,并且使用 Spring 4+,则使用标准的 JDK 反射 API 来发现参数名。这对类和接口都有效。

  • 最后,如果代码是用调试符号编译的,那么将使用调试符号来发现参数名称。这将不适用于接口,因为它们没有关于参数名称的调试信息。对于接口,必须使用注释或 JDK8 方法。

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

爪哇

@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之类的表达式。

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

# 使用 @prefilter 和 @postfilter 进行过滤

Spring 安全性支持使用表达式对集合、阵列、地图和流进行过滤。这通常是在方法的返回值上执行的。例如:

爪哇

@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 会遍历返回的集合或映射,并删除所提供的表达式为 false 的任何元素。对于数组,将返回一个包含筛选元素的新数组实例。名称filterObject是指集合中的当前对象。在使用映射的情况下,它将引用当前的Map.Entry对象,该对象允许在 expresion 中使用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 权限。传统上,这是对象的 爪哇 类,但只要与加载权限的方式一致,就不必这样做。

要使用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 (opens new window)示例应用程序配置。

# 方法安全元注释

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

@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 安全方法中的任何安全注释都可以使用元注释。为了保持与规范的兼容性,JSR-250 注释不支持元注释。

使用 FilterSecurityInterceptor 授权 HTTP 请求安全对象实现