11. Authorization

Spring Security 中的高级授权功能代表了其受欢迎程度的最令人信服的原因之一。无论选择哪种身份验证方式(使用 Spring Security 提供的机制和提供程序,还是与容器或其他非 Spring Security 身份验证机构集成),您都会发现可以在应用程序中一致且简单地使用授权服务。方式。

在这一部分中,我们将探讨在第 I 部分中介绍的不同的AbstractSecurityInterceptor实现。然后,我们将 continue 探讨如何通过使用域访问控制列表来微调授权。

11.1 授权架构

11.1.1 Authorities

正如我们在technical overview中看到的那样,所有Authentication实现都存储了GrantedAuthority个对象的列表。这些代表已授予委托人的权限。 GrantedAuthority对象由AuthenticationManager插入到Authentication对象中,然后在做出授权决策时由AccessDecisionManager读取。

GrantedAuthority是只有一种方法的接口:

String getAuthority();

此方法允许AccessDecisionManager获得GrantedAuthority的精确String表示。通过将表示形式返回为String,大多数AccessDecisionManager可以很容易地“读取” GrantedAuthority。如果GrantedAuthority不能精确地表示为String,则GrantedAuthority被视为“复杂”,并且getAuthority()必须返回null

“复杂” GrantedAuthority的示例是一种存储适用于不同 Client 帐号的操作和权限阈值列表的实现。将复杂的GrantedAuthority表示为String将会非常困难,因此getAuthority()方法应返回null。这将向任何AccessDecisionManager表示需要特别支持GrantedAuthority实现,以便理解其内容。

Spring Security 包含一个具体的GrantedAuthority实现SimpleGrantedAuthority。这允许将任何用户指定的String转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority填充Authentication对象。

11.1.2 调用前处理

正如我们在Technical Overview章中所看到的那样,Spring Security 提供了拦截器,该拦截器控制对安全对象(例如方法调用或 Web 请求)的访问。 AccessDecisionManager决定是否允许进行调用。

The AccessDecisionManager

AccessDecisionManagerAbstractSecurityInterceptor调用,并负责做出最终的访问控制决策。 AccessDecisionManager接口包含三种方法:

void decide(Authentication authentication, Object secureObject,
    Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

传递AccessDecisionManagerdecide方法所需的所有相关信息,以进行授权决策。特别是,传递安全Object可以检查实际安全对象调用中包含的那些参数。例如,假设安全对象是MethodInvocation。在MethodInvocation中查询任何Customer参数,然后在AccessDecisionManager中实现某种安全性逻辑以确保允许委托人对该 Client 进行操作将很容易。如果访问被拒绝,则预期实现会抛出AccessDeniedException

AbstractSecurityInterceptor在启动时会调用supports(ConfigAttribute)方法,以确定AccessDecisionManager是否可以处理传递的ConfigAttribute。安全拦截器实现调用supports(Class)方法,以确保已配置的AccessDecisionManager支持安全拦截器将呈现的安全对象的类型。

基于投票的 AccessDecisionManager 实现

尽管用户可以实现自己的AccessDecisionManager来控制授权的各个方面,但 Spring Security 包括几种基于投票的AccessDecisionManager实现。 图 11.1,“投票决策 Management 器”说明了相关的类。

图 11.1. 投票决策 Manager

访问决定投票

使用此方法,将根据授权决策轮询一系列AccessDecisionVoter实现。然后AccessDecisionManager根据其对投票的评估来决定是否抛出AccessDeniedException

AccessDecisionVoter界面具有三种方法:

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

具体的实现返回int,可能的值反映在AccessDecisionVoter静态字段ACCESS_ABSTAINACCESS_DENIEDACCESS_GRANTED中。如果投票实施对授权决定没有意见,则将返回ACCESS_ABSTAIN。如果确实有意见,则必须返回ACCESS_DENIEDACCESS_GRANTED

Spring Security 提供了三个具体的AccessDecisionManager来对选票进行汇总。 ConsensusBased实现将基于未弃权票的共识来授予或拒绝访问权限。提供属性以控制在票数相等或所有票都弃权的情况下的行为。如果收到一个或多个ACCESS_GRANTED投票,则AffirmativeBased实现将授予访问权限(即,如果有至少一个授予投票,则拒绝投票将被忽略)。与ConsensusBased实现类似,有一个参数可以控制所有投票者弃权时的行为。 UnanimousBased提供者希望获得一致的ACCESS_GRANTED投票,才能授予访问权限,而忽略弃权。如果有ACCESS_DENIED票,它将拒绝访问。与其他实现一样,如果所有投票者都弃权,则有一个控制行为的参数。

可以实现自定义AccessDecisionManager,以不同的方式计算票数。例如,来自特定AccessDecisionVoter的选票可能会获得额外的权重,而来自特定选民的拒绝选票可能会产生否决权。

RoleVoter

Spring Security 附带的最常用的AccessDecisionVoter是简单的RoleVoter,它会将配置属性视为简单的角色名称,并投票给用户(如果已为该用户分配了该角色)。

如果任何ConfigAttribute以前缀ROLE_开头,它将投票。如果有GrantedAuthority返回(通过getAuthority()方法通过getAuthority()精确地等于一个或多个ConfigAttributes)的GrantedAuthority表示形式,它将投票授予访问权限。如果没有任何与ROLE_开头的ConfigAttribute完全匹配,则RoleVoter将投票拒绝访问。如果没有ConfigAttributeROLE_开头,则投票者将弃权。

AuthenticatedVoter

我们暗中看到的另一个投票者是AuthenticatedVoter,它可以用来区分匿名用户,完全认证用户和记住我认证用户。许多站点允许使用“记住我”身份验证进行某些受限访问,但是要求用户通过登录以进行完全访问来确认其身份。

当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY授予匿名访问权限时,该属性已由AuthenticatedVoter处理。有关更多信息,请参见 Javadoc。

Custom Voters

显然,您还可以实现自定义AccessDecisionVoter,并且可以在其中添加几乎任何所需的访问控制逻辑。它可能特定于您的应用程序(与业务逻辑相关),也可能实现某些安全 Management 逻辑。例如,您将在 Spring 网站上找到一个blog article,其中描述了如何使用投票器实时拒绝帐户被暂停的用户的实时访问。

11.1.3 调用处理后

虽然在进行安全对象调用之前由AbstractSecurityInterceptor调用AccessDecisionManager,但是某些应用程序需要一种方法来修改安全对象调用实际返回的对象。尽管您可以轻松实现自己的 AOP 问题来实现这一点,但 Spring Security 提供了一个方便的钩子,该钩子具有一些与其 ACL 功能集成的具体实现。

图 11.2,“调用后实现”说明了 Spring Security 的AfterInvocationManager及其具体实现。

图 11.2. 调用实现后

after invocation

像 Spring Security 的许多其他部分一样,AfterInvocationManager有一个具体的实现AfterInvocationProviderManager,它轮询AfterInvocationProvider的列表。允许每个AfterInvocationProvider修改返回对象或抛出AccessDeniedException。实际上,由于前一个提供程序的结果将传递到列表中的下一个,因此多个提供程序可以修改该对象。

请注意,如果您使用的是AfterInvocationManager,则仍然需要允许MethodSecurityInterceptorAccessDecisionManager进行操作的配置属性。如果您使用的是典型的 Spring Security 包含的AccessDecisionManager实现,则没有为特定的安全方法调用定义配置属性,将导致每个AccessDecisionVoter放弃投票。反过来,如果AccessDecisionManager属性“ allowIfAllAbstainDecisions”为false,则会抛出AccessDeniedException。您可以通过(i)将“ allowIfAllAbstainDecisions”设置为true(尽管通常不建议这样做)或(ii)简单地确保至少有一个配置属性供AccessDecisionVoter投票以授予访问权限,从而避免出现此潜在问题。后一种(推荐)方法通常通过ROLE_USERROLE_AUTHENTICATED配置属性来实现。

11.1.4 层次角色

通常要求应用程序中的特定角色应自动“包括”其他角色。例如,在具有“Management 员”和“用户”角色概念的应用程序中,您可能希望 Management 员能够执行普通用户可以执行的所有操作。为此,您可以确保还为所有 Management 员用户分配了“用户”角色。或者,您可以修改每个需要“用户”角色也要包括“Management 员”角色的访问约束。如果您的应用程序中有很多不同的角色,这可能会变得非常复杂。

使用角色层次结构可让您配置哪些角色(或权限)应包括其他角色。 Spring Security 的RoleVoterRoleHierarchyVoter的扩展版本配置了RoleHierarchy,从中可以获取为用户分配的所有“可访问权限”。典型的配置可能如下所示:

<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
    <constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    <property name="hierarchy">
        <value>
            ROLE_ADMIN > ROLE_STAFF
            ROLE_STAFF > ROLE_USER
            ROLE_USER > ROLE_GUEST
        </value>
    </property>
</bean>

在这里,我们在层次结构ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST中有四个角色。通过ROLE_ADMIN进行身份验证的用户的行为类似于针对使用以上RoleHierarchyVoter配置的AccessDecisionManager c 评估安全约束时将扮演所有四个角色。可以将>符号视为“包含”。

角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种方便的方法。对于更复杂的要求,您可能希望在应用程序需要的特定访问权限与分配给用户的角色之间定义逻辑 Map,并在加载用户信息时在两者之间进行转换。

11.2 安全对象的实现

11.2.1 AOP Alliance(方法调用)安全拦截器

在 Spring Security 2.0 之前,保护MethodInvocation s 需要大量样板配置。现在,推荐的方法安全性方法是使用namespace configuration。这样,方法安全性基础结构 bean 将自动为您配置,因此您实际上不需要了解实现类。我们将仅简要介绍此处涉及的类。

方法安全性是使用MethodSecurityInterceptor强制执行的,它可以保护MethodInvocation s。取决于配置方法,拦截器可能特定于单个 bean,也可能在多个 bean 之间共享。拦截器使用MethodSecurityMetadataSource实例获取适用于特定方法调用的配置属性。 MapBasedMethodSecurityMetadataSource用于存储以方法名称作为关键字的配置属性(可以通配),当使用<intercept-methods><protect-point>元素在应用程序上下文中定义属性时,它将在内部使用。其他实现将用于处理基于 Comments 的配置。

显式 MethodSecurityInterceptor 配置

当然,您可以直接在应用程序上下文中配置MethodSecurityIterceptor,以与 Spring AOP 的代理机制之一配合使用:

<bean id="bankManagerSecurity" class=
    "org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
    <sec:method-security-metadata-source>
    <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
    <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
    </sec:method-security-metadata-source>
</property>
</bean>

11.2.2 AspectJ(JoinPoint)安全拦截器

AspectJ 安全拦截器与上一节中讨论的 AOP Alliance 安全拦截器非常相似。实际上,我们将仅讨论本节中的区别。

AspectJ 拦截器的名称为AspectJSecurityInterceptor。与 AOP Alliance 安全拦截器不同,后者依赖于 Spring 应用程序上下文通过代理在安全拦截器中进行编织,而AspectJSecurityInterceptor是通过 AspectJ 编译器进行编织的。在同一应用程序中同时使用两种类型的安全拦截器并不少见,其中AspectJSecurityInterceptor用于域对象实例安全,而 AOPunionMethodSecurityInterceptor用于服务层安全。

让我们首先考虑如何在 Spring 应用程序上下文中配置AspectJSecurityInterceptor

<bean id="bankManagerSecurity" class=
    "org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
    <sec:method-security-metadata-source>
    <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
    <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
    </sec:method-security-metadata-source>
</property>
</bean>

如您所见,除了类名之外,AspectJSecurityInterceptor与 AOP Alliance 安全拦截器完全相同。实际上,这两个拦截器可以共享相同的securityMetadataSource,因为SecurityMetadataSourcejava.lang.reflect.Method一起使用,而不是 AOP 库特定的类。当然,您的访问决策可以访问相关的 AOP 库特定的调用(即MethodInvocationJoinPoint),因此在制定访问决策(例如方法参数)时可以考虑一系列附加条件。

接下来,您需要定义 AspectJ aspect。例如:

package org.springframework.security.samples.aspectj;

import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;

public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {

    private AspectJSecurityInterceptor securityInterceptor;

    pointcut domainObjectInstanceExecution(): target(PersistableEntity)
        && execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);

    Object around(): domainObjectInstanceExecution() {
        if (this.securityInterceptor == null) {
            return proceed();
        }

        AspectJCallback callback = new AspectJCallback() {
            public Object proceedWithObject() {
                return proceed();
            }
        };

        return this.securityInterceptor.invoke(thisJoinPoint, callback);
    }

    public AspectJSecurityInterceptor getSecurityInterceptor() {
        return securityInterceptor;
    }

    public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
        this.securityInterceptor = securityInterceptor;
    }

    public void afterPropertiesSet() throws Exception {
        if (this.securityInterceptor == null)
            throw new IllegalArgumentException("securityInterceptor required");
        }
    }
}

在上面的示例中,安全拦截器将应用于PersistableEntity的每个实例,这是一个未显示的抽象类(您可以使用任意其他类或pointcut表达式)。对于那些好奇的人,需要AspectJCallback,因为proceed();语句仅在around()主体内具有特殊含义。 AspectJSecurityInterceptor在希望目标对象 continue 时调用此匿名AspectJCallback类。

您将需要配置 Spring 以加载方面并将其与AspectJSecurityInterceptor连接。实现此目的的 bean 声明如下所示:

<bean id="domainObjectInstanceSecurityAspect"
    class="security.samples.aspectj.DomainObjectInstanceSecurityAspect"
    factory-method="aspectOf">
<property name="securityInterceptor" ref="bankManagerSecurity"/>
</bean>

而已!现在,您可以使用自己认为合适的任何方式(例如new Person();)从应用程序中的任何位置创建 bean,并且将应用安全拦截器。

11.3 基于表达式的访问控制

Spring Security 3.0 引入了使用 Spring EL 表达式作为授权机制的功能,此外还可以简单地使用配置属性和访问决定投票器。基于表达式的访问控制基于相同的体系结构,但是允许将复杂的布尔逻辑封装在单个表达式中。

11.3.1 Overview

Spring Security 使用 Spring EL 来支持表达,如果您想更深入地了解该主题,则应该看看它的工作方式。使用“根对象”评估表达式作为评估上下文的一部分。 Spring Security 使用特定的类作为 Web 对象和方法的安全性作为根对象,以提供内置的表达式并访问诸如当前主体的值。

通用内置表达式

表达式根对象的 Base Class 为SecurityExpressionRoot。这提供了 Web 和方法安全性中都可用的一些常用表达式。

表 11.1. 常见的内置表达式

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

11.3.2 Web 安全表达式

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

<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中。因此,如果您不使用名称空间而要使用表达式,则必须将其中之一添加到配置中。

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

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

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

11.3.3 方法安全性表达式

方法安全性比简单的允许或拒绝规则要复杂一些。为了提供对表达式使用的全面支持,Spring Security 3.0 引入了一些新的 Comments。

@Pre 和@Post 注解

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

<global-method-security pre-post-annotations="enabled"/>
使用@PreAuthorize 和@PostAuthorize 进行访问控制

最明显有用的 Comments 是@PreAuthorize,它决定是否可以实际调用方法。例如(来自“联系人”示例应用程序)

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

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

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

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

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

  • 如果 Spring Security 的@PComments 出现在方法的单个参数上,则将使用该值。这对于使用 JDK 8 之前的 JDK 编译的接口非常有用,该接口不包含有关参数名称的任何信息。例如:
import org.springframework.security.access.method.P;

...

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

在后台使用AnnotationParameterNameDiscoverer实现此用法,可以对其进行自定义以支持任何指定 Comments 的 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实现此用法,可以对其进行自定义以支持任何指定 Comments 的 value 属性。

  • 如果使用 JDK 8 通过-parameters 参数编译源,并且使用 Spring 4,则将使用标准 JDK 反射 API 来发现参数名称。这适用于类和接口。

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

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

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

在这里,我们正在访问另一个内置表达式authentication,它是存储在安全上下文中的Authentication。您也可以使用表达式principal直接访问其“主要”属性。该值通常是UserDetails实例,因此您可以使用principal.usernameprincipal.enabled之类的表达式。

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

使用@PreFilter 和@PostFilter 进行过滤

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

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

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

请注意,过滤显然不能代替调整数据检索查询。如果要过滤大型集合并删除许多条目,则效率可能很低。

Built-In Expressions

有一些特定于方法安全性的内置表达式,我们已经在上面使用过。 filterTargetreturnValue值很简单,但是使用hasPermission()表达式需要仔细观察。

PermissionEvaluator 界面

hasPermission()表达式委托给PermissionEvaluator的实例。它旨在在表达式系统和 Spring Security 的 ACL 系统之间架起 bridge 梁,使您可以基于抽象权限在域对象上指定授权约束。它对 ACL 模块没有明确的依赖关系,因此如果需要,您可以将其换成其他实现。该接口有两种方法:

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

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

除了未提供第一个参数(Authentication对象)之外,该参数直接 Map 到表达式的可用版本。第一种方法用于已经控制了访问权限的域对象已加载的情况。如果当前用户对该对象具有给定的权限,则 expression 将返回 true。第二种版本用于未加载对象但已知其标识符的情况。还需要域对象的抽象“类型”说明符,以允许加载正确的 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。通常,这将是来自称为AclPermissionEvaluator的 ACL 模块的实现。有关更多详细信息,请参见“联系人”示例应用程序配置。

方法安全性元 Comments

您可以使用元 Comments 来保证方法的安全性,以使代码更具可读性。如果发现在整个代码库中重复相同的复杂表达式,这将特别方便。例如,考虑以下内容:

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

不必在所有地方重复此操作,我们可以创建一个可以用作替代的元 Comments。

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

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