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

基于表达式的访问控制

概述

Spring Security 使用 SpEL 来支持表达式,如果您有兴趣更深入地了解该主题,则应了解其工作原理。 表达式使用“根对象”作为计算上下文的一部分进行计算。 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对象从SecurityContext.spring-doc.cadn.net.cn

permitAllspring-doc.cadn.net.cn

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

denyAllspring-doc.cadn.net.cn

Always 的计算结果为false.spring-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如果用户不是匿名用户,也不是 Remember-Me 用户。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>元素来包含 SPEL 表达式。 每个表达式的计算结果应为布尔值,定义是否应允许访问。 下面的清单显示了一个示例: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表达式。 这hasIpAddressexpression 是特定于 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
        .requestMatchers("/user/**").access(new WebExpressionAuthorizationManager("@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
		.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@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 注释

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

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

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

最明显有用的注释是@PreAuthorize,它决定方法是否真的可以调用。 以下示例(来自 “Contacts” 示例应用程序)使用@PreAuthorize注解:spring-doc.cadn.net.cn

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

这意味着仅允许具有ROLE_USER角色。 显然,通过使用 required 角色的传统配置和简单的配置属性,可以很容易地实现相同的目的。 但是,请考虑以下示例: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?)

在这里,我们实际上使用方法参数作为表达式的一部分来决定当前用户是否具有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 编译的接口(不包含有关参数名称的任何信息)非常有用。 以下示例使用@P注解: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?)

    Behind the scenes, this is implemented by using AnnotationParameterNameDiscoverer, which you can customize to support the value attribute of any specified annotation.spring-doc.cadn.net.cn

  • If Spring Data’s @Param annotation is present on at least one parameter for the method, the value is used. This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names. The following example uses the @Param annotation: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?

    Behind the scenes, this is implemented by using AnnotationParameterNameDiscoverer, which you can customize to support the value attribute of any specified annotation.spring-doc.cadn.net.cn

  • If JDK 8 was used to compile the source with the -parameters argument and Spring 4+ is being used, the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces.spring-doc.cadn.net.cn

  • Finally, if the code was compiled with the debug symbols, the parameter names are discovered by using the debug symbols. This does not work for interfaces, since they do not have debug information about the parameter names. For interfaces, annotations or the JDK 8 approach must be used.spring-doc.cadn.net.cn

Any SpEL functionality is available within the expression, so you can also access properties on the arguments. For example, if you wanted a particular method to allow access only to a user whose username matched that of the contact, you could writespring-doc.cadn.net.cn

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

Here, we access another built-in expression, authentication, which is the Authentication stored in the security context. You can also access its principal property directly, by using the principal expression. The value is often a UserDetails instance, so you might use an expression such as principal.username or principal.enabled.spring-doc.cadn.net.cn

Filtering using @PreFilter and @PostFilter

Spring Security supports filtering of collections, arrays, maps, and streams by using expressions. This is most commonly performed on the return value of a method. The following example uses @PostFilter: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?>

When using the @PostFilter annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false. For an array, a new array instance that contains filtered elements is returned. filterObject refers to the current object in the collection. When a map is used, it refers to the current Map.Entry object, which lets you use filterObject.key or filterObject.value in the expression. You can also filter before the method call by using @PreFilter, though this is a less common requirement. The syntax is the same. However, if there is more than one argument that is a collection type, you have to select one by name using the filterTarget property of this annotation.spring-doc.cadn.net.cn

Note that filtering is obviously not a substitute for tuning your data retrieval queries. If you are filtering large collections and removing many of the entries, this is likely to be inefficient.spring-doc.cadn.net.cn

Built-In Expressions

There are some built-in expressions that are specific to method security, which we have already seen in use earlier. The filterTarget and returnValue values are simple enough, but the use of the hasPermission() expression warrants a closer look.spring-doc.cadn.net.cn

The PermissionEvaluator interface

hasPermission() expressions are delegated to an instance of PermissionEvaluator. It is intended to bridge between the expression system and Spring Security’s ACL system, letting you specify authorization constraints on domain objects, based on abstract permissions. It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required. The interface has two methods:spring-doc.cadn.net.cn

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

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

These methods map directly to the available versions of the expression, with the exception that the first argument (the Authentication object) is not supplied. The first is used in situations where the domain object, to which access is being controlled, is already loaded. Then the expression returns true if the current user has the given permission for that object. The second version is used in cases where the object is not loaded but its identifier is known. An abstract “type” specifier for the domain object is also required, letting the correct ACL permissions be loaded. This has traditionally been the Java class of the object but does not have to be, as long as it is consistent with how the permissions are loaded.spring-doc.cadn.net.cn

To use hasPermission() expressions, you have to explicitly configure a PermissionEvaluator in your application context. The following example shows how to do so: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>

Where myPermissionEvaluator is the bean which implements PermissionEvaluator. Usually, this is the implementation from the ACL module, which is called AclPermissionEvaluator. See the Contacts sample application configuration for more details.spring-doc.cadn.net.cn

Method Security Meta Annotations

You can make use of meta annotations for method security to make your code more readable. This is especially convenient if you find that you repeat the same complex expression throughout your code base. For example, consider the following:spring-doc.cadn.net.cn

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

Instead of repeating this everywhere, you can create a meta annotation: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

You can use meta annotations for any of the Spring Security method security annotations. To remain compliant with the specification, JSR-250 annotations do not support meta annotations.spring-doc.cadn.net.cn