26. Expression-Based Access Control(基于表达式的访问控制)
Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,此外还有配置属性和访问决策投票者的简单使用,这在前面已经看到过。基于表达式的访问控制建立在相同的体系结构上,但是允许复杂的布尔逻辑封装在单个表达式中。
26.1 Overview
Spring Security使用Spring EL来支持表达式,如果您有兴趣更深入地理解这个主题,您应该看看它是如何工作的。表达式用“根对象”作为评估上下文的一部分进行评估。Spring Security使用特定的web和方法安全类作为根对象,以便提供内置表达式和对当前主体等值的访问。
26.1.1 Common Built-In Expressions(常见内置表达式)
表达式根对象的基类是安全表达式根(SecurityExpressionRoot)。这提供了一些在web和方法安全性中都可用的通用表达式。
hasRole([role]) :如果当前主体具有指定的角色,则返回true。默认情况下,如果提供的角色不以“ROLE_”开头,它将被添加。这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。
hasAnyRole([role1,role2]):如果当前主体具有任何提供的角色(以逗号分隔的字符串列表形式给出),则返回true。默认情况下,如果提供的角色不以“ROLE_”开头,它将被添加。这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。
hasAuthority([authority]) :如果当前主体具有指定的权限,则返回true。
hasAnyAuthority([authority1,authority2]):如果当前主体具有任何提供的角色(以逗号分隔的字符串列表形式给出),则返回true。
principal:允许直接访问代表当前用户的主体对象。
authentication:允许直接访问从安全上下文获得的当前身份验证对象。
permitAll:总是评估为真。
denyAll:总是计算为假。
isAnonymous():如果当前主体是匿名用户,则返回true。
isRememberMe():如果当前主体是“记住我”用户,则返回true。
isAuthenticated():如果用户不是匿名的,则返回true。
isFullyAuthenticated():如果用户不是匿名用户或记得我的用户,则返回true。
hasPermission(Object target, Object permission):如果用户可以访问给定权限下提供的目标,则返回true。例如,hasPermission(域对象,“读取”)。
hasPermission(Object targetId, String targetType, Object permission):如果用户可以访问给定权限下提供的目标,则返回true。例如,hasPermission(1,“com.example.domain.Message”,“read”)
26.2 Web Security Expressions(网络安全表达式)
要使用表达式来保护单个网址,首先需要将< http >元素中的use-expressions属性设置为true。然后,Spring Security将期望<intercept-url >元素的访问属性包含Spring EL表达式。表达式应该计算为布尔值,定义是否允许访问。例如:
在这里,我们已经定义了应用程序的“admin”区域(由网址模式定义)应该只对拥有“admin”权限并且其IP地址与本地子网匹配的用户可用。我们已经在上一节中看到了内置的hasRole表达式。hasIpAddress表达式是一个额外的内置表达式,专用于网络安全。它由WebSecurityExpressionRoot类定义,该类的一个实例在计算网络访问表达式时用作表达式根对象。该对象还在名称请求下直接公开了HttpServletRequest对象,因此您可以在表达式中直接调用该请求。如果正在使用表达式,将向命名空间使用的访问决策管理器(AccessDecisionManager )中添加一个网络表达式(WebExpressionVoter )。因此,如果您不使用命名空间,并且想要使用表达式,那么您必须将其中一个添加到配置中。
26.2.1 Referring to Beans in Web Security Expressions(在网络安全表达式中引用Beans)
如果您希望扩展可用的表达式,您可以很容易地引用您公开的任何Spring Bean。例如,假设您有一个名为网络安全的Bean,它包含以下方法签名:
您可以使用以下方法来引用该方法:
26.2.2 Path Variables in Web Security Expressions(网络安全表达式中的路径变量)
有时能够在一个网址中引用路径变量是很好的。例如,考虑一个RESTful应用程序,它从格式为/user/{userId}的网址路径中按id查找用户。
通过将路径变量放在模式中,可以很容易地引用它。例如,如果您有一个名为网络安全的Bean,它包含以下方法签名:
您可以使用以下方法来引用该方法:
或者在Java配置中
在这两种配置中,匹配的网址会将路径变量传入(并将其转换为)checkUserId方法。例如,如果网址是/user/123/resource,那么传入的id将是123。
26.3 Method Security Expressions(方法安全表达式)
方法安全性比简单的允许或拒绝规则要复杂一些。Spring Security 3.0引入了一些新的注释,以便对表达式的使用提供全面的支持。
26.3.1 @Pre and @Post Annotations
有四个注释支持表达式属性,以允许调用前和调用后的授权检查,并支持对提交的集合参数或返回值进行过滤。它们是@PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter。它们的使用是通过全局方法安全命名(global-method-security)空间元素实现的:
<global-method-securitypre-post-annotations="enabled"/>
Access Control using @PreAuthorize and @PostAuthorize
最明显有用的注释是@PreAuthorize,它决定了一个方法是否可以被调用。例如(来自“联系人”示例应用程序)
这意味着只有角色为“ROLE_USER”的用户才允许访问。显然,对于所需的角色,使用传统的配置和简单的配置属性可以很容易地实现同样的事情。但是关于:
在这里,我们实际上使用了一个方法参数作为表达式的一部分来决定当前用户是否拥有给定联系人的“管理员”权限。内置的hasPermission()表达式通过应用程序上下文链接到Spring安全性ACL模块,如下所述。您可以通过名称作为表达式变量来访问任何方法参数。
Spring Security可以通过多种方式解析方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer来发现参数名称。默认情况下,对整个方法尝试下列选项。
如果Spring Security的@P注释出现在该方法的单个参数上,将使用该值。这对于在JDK 8之前用JDK编译的接口很有用,它不包含任何关于参数名的信息。例如:
在幕后,这种使用是使用AnnotationParameterNameDiscoverer实现的,它可以被定制以支持任何指定注释的值属性。
如果该方法的至少一个参数上存在Spring 数据的@Param注释,则将使用该值。这对于在JDK 8之前用JDK编译的接口很有用,它不包含任何关于参数名的信息。例如:
在幕后,这种使用是使用AnnotationParameterNameDiscoverer实现的,它可以被定制以支持任何指定注释的值属性。
如果JDK 8被用来编译带有-parameters参数的源代码,而Spring 4+被使用,那么标准的JDK反射API被用来发现参数名。这适用于类和接口。
最后,如果代码是用debug编译的,将使用debug发现参数名称。这不适用于接口,因为它们没有关于参数名的调试信息。对于接口,必须使用注释或JDK 8方法。
表达式中提供了任何Spring-EL功能,因此您也可以访问参数的属性。例如,如果您希望某个特定方法只允许用户名与联系人匹配的用户访问,您可以写
在这里,我们访问另一个内置表达式,身份验证,它是存储在安全上下文中的身份验证。您也可以使用表达式principal直接访问它的“principal”属性。该值通常是一个用户详细信息实例,因此您可以使用像principal.username或principal.enabled这样的表达式。
不太常见的是,您可能希望在调用方法后执行访问控制检查。这可以通过@ PostAuthorize注释来实现。若要从方法访问返回值,请在表达式中使用内置名称returnObject。
Filtering using @PreFilter and @PostFilter
您可能已经知道,Spring Security支持集合和数组的过滤,现在可以使用表达式来实现这一点。这通常在方法的返回值上执行。例如:
当使用@PostFilter注释时,Spring Security会遍历返回的集合,并移除提供的表达式为false的所有元素。名称filterObject引用集合中的当前对象。您也可以在方法调用之前使用@PreFilter进行过滤,尽管这不是一个常见的要求。语法是一样的,但是如果有一个以上的参数是集合类型,那么您必须使用这个注释的filterTarget属性按名称选择一个。
26.3.2 Built-In Expressions
有一些特定于方法安全性的内置表达式,我们已经在上面的使用中看到了。filterTarget和returnValue值很简单,但是hasPermission()表达式的使用保证了更仔细的观察。
The PermissionEvaluator interface
hasPermission()表达式被委托给PermissionEvaluator的一个实例。它旨在连接表达式系统和Spring Security的ACL系统,允许您根据抽象权限指定对域对象的授权约束。它对ACL模块没有明确的依赖关系,因此如果需要,您可以将它换成另一种实现。该接口有两种方法:
它直接映射到表达式的可用版本,只是没有提供第一个参数(身份验证对象)。第一种用于域对象已经被加载的情况,对域对象的访问是受控的。如果当前用户拥有该对象的给定权限,则表达式将返回true。第二个版本用于未加载对象,但其标识符已知的情况。还需要域对象的抽象“类型”说明符,以允许加载正确的ACL权限。这在传统上一直是对象的Java类,但只要它与权限的加载方式一致,就不必是这样。
要使用hasPermission()表达式,必须在应用程序上下文中显式配置PermissionEvaluator。这看起来像这样:
其中myPermissionEvaluator是实现PermissionEvaluator的bean。通常这将是ACL模块的实现,该模块被称为ACL权限评估器。有关更多详细信息,请参见“联系人”示例应用程序配置。
Method Security Meta Annotations
您可以利用方法安全性的元注释来提高代码的可读性。如果您发现在整个代码库中重复相同的复杂表达式,这就特别方便。例如,考虑以下情况:
我们可以创建一个可以替代使用的元注释,而不是到处重复。
元注释可以用于任何Spring Security方法安全注释。为了保持符合规范,JSR-250注释不支持元注释。