对于最新的稳定版本,请使用 Spring Security 6.5.0spring-doc.cadn.net.cn

基于表达式的访问控制

概述

Spring Security 使用 Spring EL 来支持表达式,如果您有兴趣更深入地了解该主题,您应该了解它是如何工作的。 表达式使用“根对象”作为计算上下文的一部分进行计算。 Spring Security 使用特定的 Web 类和方法安全性作为根对象,以便提供内置表达式和对值(例如当前主体)的访问。spring-doc.cadn.net.cn

常见的内置表达式

表达式根对象的基类是SecurityExpressionRoot. 这提供了一些在 Web 和方法安全性中都可用的常用表达式。spring-doc.cadn.net.cn

表 1.常见的内置表达式
表达 描述

hasRole(String role)spring-doc.cadn.net.cn

返回true如果当前主体具有指定的角色。spring-doc.cadn.net.cn

例如hasRole('admin')spring-doc.cadn.net.cn

默认情况下,如果提供的角色不以 'ROLE_' 开头,则会添加它。 这可以通过修改defaultRolePrefixDefaultWebSecurityExpressionHandler.spring-doc.cadn.net.cn

hasAnyRole(String…​ roles)spring-doc.cadn.net.cn

返回true如果当前主体具有提供的任何角色(以逗号分隔的字符串列表形式提供)。spring-doc.cadn.net.cn

例如hasAnyRole('admin', 'user')spring-doc.cadn.net.cn

默认情况下,如果提供的角色不以 'ROLE_' 开头,则会添加它。 这可以通过修改defaultRolePrefixDefaultWebSecurityExpressionHandler.spring-doc.cadn.net.cn

hasAuthority(String authority)spring-doc.cadn.net.cn

返回true如果当前主体具有指定的权限。spring-doc.cadn.net.cn

例如hasAuthority('read')spring-doc.cadn.net.cn

hasAnyAuthority(String…​ authorities)spring-doc.cadn.net.cn

返回true如果当前主体具有任何提供的授权(以逗号分隔的字符串列表形式提供)spring-doc.cadn.net.cn

例如hasAnyAuthority('read', 'write')spring-doc.cadn.net.cn

principalspring-doc.cadn.net.cn

允许直接访问表示当前用户的主体对象spring-doc.cadn.net.cn

authenticationspring-doc.cadn.net.cn

允许直接访问当前的Authentication对象从SecurityContextspring-doc.cadn.net.cn

permitAllspring-doc.cadn.net.cn

Always 的计算结果为truespring-doc.cadn.net.cn

denyAllspring-doc.cadn.net.cn

Always 的计算结果为falsespring-doc.cadn.net.cn

isAnonymous()spring-doc.cadn.net.cn

返回true如果当前主体是匿名用户spring-doc.cadn.net.cn

isRememberMe()spring-doc.cadn.net.cn

返回true如果当前主体是 Remember-Me 用户spring-doc.cadn.net.cn

isAuthenticated()spring-doc.cadn.net.cn

返回true如果用户不是匿名的spring-doc.cadn.net.cn

isFullyAuthenticated()spring-doc.cadn.net.cn

返回true如果用户不是匿名用户或记住我用户spring-doc.cadn.net.cn

hasPermission(Object target, Object permission)spring-doc.cadn.net.cn

返回true如果用户有权访问为给定权限提供的目标。 例如hasPermission(domainObject, 'read')spring-doc.cadn.net.cn

hasPermission(Object targetId, String targetType, Object permission)spring-doc.cadn.net.cn

返回true如果用户有权访问为给定权限提供的目标。 例如hasPermission(1, 'com.example.domain.Message', 'read')spring-doc.cadn.net.cn

Web 安全表达式

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

<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 访问表达式时,其实例用作表达式根对象。 这个对象还直接暴露了HttpServletRequest名称下的 objectrequest,因此您可以直接在表达式中调用请求。 如果正在使用表达式,则WebExpressionVoter将添加到AccessDecisionManager,该名称空间使用。 因此,如果您不使用命名空间并希望使用表达式,则必须将其中一个表达式添加到您的配置中。spring-doc.cadn.net.cn

在 Web 安全表达式中引用 Bean

如果你希望扩展可用的表达式,你可以很容易地引用你公开的任何 Spring Bean。 例如,假设您有一个名称为webSecurity,其中包含以下方法签名:spring-doc.cadn.net.cn

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

您可以使用以下方法引用该方法:spring-doc.cadn.net.cn

参考方法
http
    .authorizeHttpRequests(authorize -> authorize
        .antMatchers("/user/**").access("@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}.spring-doc.cadn.net.cn

您可以通过将 path 变量放置在 pattern 中来轻松引用它。 例如,如果你有一个名称为webSecurity,其中包含以下方法签名:spring-doc.cadn.net.cn

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

您可以使用以下方法引用该方法:spring-doc.cadn.net.cn

路径变量
http
	.authorizeHttpRequests(authorize -> authorize
		.antMatchers("/user/{userId}/**").access("@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-doc.cadn.net.cn

方法安全表达式

方法安全性比简单的允许或拒绝规则要复杂一些。 Spring Security 3.0 引入了一些新的 Comments,以便全面支持表达式的使用。spring-doc.cadn.net.cn

@Pre 和 @Post 注释

有四个注释支持 expression 属性,以允许调用前和调用后授权检查,还支持筛选提交的集合参数或返回值。 他们是@PreAuthorize,@PreFilter,@PostAuthorize@PostFilter. 它们的使用是通过global-method-securitynamespace 元素:spring-doc.cadn.net.cn

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

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

最明显有用的注释是@PreAuthorize它决定了方法是否真的可以被调用。 例如(来自 Contacts 示例应用程序)spring-doc.cadn.net.cn

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

这意味着仅允许具有“ROLE_USER”角色的用户访问。 显然,使用传统配置和所需角色的简单配置属性可以很容易地实现相同的作。 但是呢:spring-doc.cadn.net.cn

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

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

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

  • 如果 Spring Security 的@Pannotation 存在于该方法的单个参数上,则将使用该值。 这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,这些接口不包含有关参数名称的任何信息。 例如:spring-doc.cadn.net.cn

    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可以自定义该 Comments,以支持任何指定 Comments 的 value 属性。spring-doc.cadn.net.cn

  • 如果 Spring Data 的@Paramannotation 的 Comments 存在于该方法的至少一个参数上,则将使用该值。 这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,这些接口不包含有关参数名称的任何信息。 例如:spring-doc.cadn.net.cn

    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可以自定义该 Comments,以支持任何指定 Comments 的 value 属性。spring-doc.cadn.net.cn

  • 如果使用 JDK 8 编译带有 -parameters 参数的源代码,并且正在使用 Spring 4+,则使用标准 JDK 反射 API 来发现参数名称。 这适用于类和接口。spring-doc.cadn.net.cn

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

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

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

这里我们访问了另一个内置表达式authentication,即Authentication存储在安全上下文中。 你也可以使用表达式principal. 该值通常是UserDetails实例,因此您可以使用类似principal.usernameprincipal.enabled.spring-doc.cadn.net.cn

不太常见的是,您可能希望在调用方法后执行访问控制检查。 这可以使用@PostAuthorize注解。 要从方法访问返回值,请使用内置名称returnObject在表达式中。spring-doc.cadn.net.cn

使用 @PreFilter 和 @PostFilter 进行筛选

Spring Security 支持使用表达式过滤集合、数组、映射和流。 这通常对方法的返回值执行。 例如:spring-doc.cadn.net.cn

@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.keyfilterObject.value在解释中。 您还可以在方法调用之前使用@PreFilter,尽管这是一个不太常见的要求。 语法是相同的,但是如果有多个参数是集合类型,则必须使用filterTarget属性。spring-doc.cadn.net.cn

请注意,筛选显然不能替代优化数据检索查询。 如果要筛选大型集合并删除许多条目,则这可能效率低下。spring-doc.cadn.net.cn

内置表达式

有一些特定于方法安全性的内置表达式,我们已经在上面看到了这些表达式。 这filterTargetreturnValue值很简单,但是使用hasPermission()表达值得仔细观察。spring-doc.cadn.net.cn

PermissionEvaluator 接口

hasPermission()表达式被委托给PermissionEvaluator. 它旨在桥接表达式系统和 Spring Security 的 ACL 系统,允许您根据抽象权限在域对象上指定授权约束。 它对 ACL 模块没有明确的依赖关系,因此如果需要,您可以将其换成替代实现。 该接口有两种方法:spring-doc.cadn.net.cn

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

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

直接映射到表达式的可用版本,但第一个参数(Authenticationobject) 的 第一种用于已加载 domain 对象(其访问权限被控制)的情况。 如果当前用户具有该对象的给定权限,则 expression 将返回 true。 第二个版本用于对象未加载但其标识符已知的情况下。 还需要域对象的抽象 “type” 说明符,以允许加载正确的 ACL 权限。 传统上,这是对象的 Java 类,但只要它与权限的加载方式一致,就不必如此。spring-doc.cadn.net.cn

要使用hasPermission()表达式中,您必须显式配置PermissionEvaluator在您的应用程序上下文中。 这将如下所示:spring-doc.cadn.net.cn

<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. 请参阅 联系人 示例应用程序配置 以了解更多详细信息。spring-doc.cadn.net.cn

方法安全元注释

您可以利用元注释来实现方法安全性,以使您的代码更具可读性。 如果您发现在整个代码库中重复相同的复杂表达式,这将特别方便。 例如,请考虑以下情况:spring-doc.cadn.net.cn

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

与其到处重复此作,不如创建一个可以使用的 meta 注释。spring-doc.cadn.net.cn

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

元注释可用于任何 Spring Security 方法安全注释。 为了保持与规范的兼容性,JSR-250 注解不支持元注解。spring-doc.cadn.net.cn