# 14.2.2　过滤方法的输入和输出

如果我们希望使用表达式来保护方法的话，那使用 @PreAuthorize 和 @PostAuthorize 是非常好的方案。但是，有时候限制方法调用太严格了。有时，需要保护的并不是对方法的调用，需要保护的是传入方法的数据和方法返回的数据。

例如，我们有一个名为 getOffensiveSpittles() 的方法，这个方法会返回标记为具有攻击性的 Spittle 列表。这个方法主要会给管理员使用，以保证 Spittr 应用中内容的和谐。但是，普通用户也可以使用这个方法，用来查看他们所发布的 Spittle 有没有被标记为具有攻击性。这个方法的签名大致如下所示：

```java
public List<Spittle> getOffensiveSpittles() { ... }
```

按照这种方法的定义，getOffensiveSpittles() 方法与具体的用户并没有关联。它只会返回攻击性 Spittle 的一个列表，并不关心它们属于哪个用户。对于管理员使用来说，这是一个很好的方法，但是它无法限制列表中的 Spittle 都属于当前用户。

当然，我们也可以重载 getOffensiveSpittles()，实现另一个版本，让它接受一个用户 ID 作为参数，查询给定用户的 Spittle。但是，正如我在本章开头所讲的那样，始终会有这样的可能性，那就是将较为宽松限制的版本用在具有一定安全限制的场景中。

我们需要有一种方式过滤 getOffensiveSpittles() 方法返回的 Spittle 集合，将结果限制为允许当前用户看到的内容，而这就是 Spring Security 的 @PostFilter 所能做的事情。我们来试一下。

**事后对方法的返回值进行过滤**

与 @PreAuthorize 和 @PostAuthorize 类似，@PostFilter 也使用一个 SpEL 作为值参数。但是，这个表达式不是用来限制方法访问的，@PostFilter 会使用这个表达式计算该方法所返回集合的每个成员，将计算结果为 false 的成员移除掉。

为了阐述该功能，我们将 @PostFilter 应用在 getOffensiveSpittles() 方法上：

```java
@PreAuthorize("hasRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PostFilter("hasRole('ROLE_ADMIN') || filterObject.spitter.username == principal.username")
public List<Spittle> getOffensiveSpittles() {
  ...
}
```

在这里，@PreAuthorize 限制只有具备 ROLE\_SPITTER 或 ROLE\_ADMIN 权限的用户才能访问该方法。如果用户能够通过这个检查点，那么方法将会执行，并且会返回 Spittle 所组成的一个 List。但是，@PostFilter 注解将会过滤这个列表，确保用户只 能看到允许的 Spittle。具体来讲，管理员能够看到所有攻击性的 Spittle，非管理员只能看到属于自己的 Spittle。

表达式中的 filterObject 对象引用的是这个方法所返回 List 中的某一个元素（我们知道它是一个 Spittle）。在这个 Spittle 对象中，如果 Spitter 的用户名与认证用户（表达式中的 principal.name） 相同或者用户具有 ROLE\_ADMIN 角色，那这个元素将会最终包含在过滤后的列表中。否则，它将被过滤掉。

**事先对方法的参数进行过滤**

除了事后过滤方法的返回值，我们还可以预先过滤传入到方法中的值。这项技术不太常用，但是在有些场景下可能会很便利。例如，假设我们希望以批处理的方式删除 Spittle 组成的列表。为了完成该功能，我们可能会编写一个方法，其签名大致如下所示：

```java
public void deleteSpittles(List<Spittle> spittles) { ... }
```

看起来很简单，对吧？但是，如果我们想在它上面应用一些安全规则的话，比如 Spittle 只能由其所有者或管理员删除，那该怎么做呢？如果是这样的话，我们可以将逻辑放在 deleteSpittles() 方法中，在这里循环列表中的 Spittle，只删除属于当前用户的那一部分对象（如果当前用户是管理员的话，则会全部删除）。

这能够运行正常，但是这意味着我们需要将安全逻辑直接嵌入到方法之中。相对于删除 Spittle 来讲，安全逻辑是独立的关注点（当然，它们也有所关联）。如果列表中能够只包含实际要删除的 Spittle，这样会更好一些，因为这能帮助 deleteSpittles() 方法中的逻辑更加简单，只关注于删除 Spittle 的任务。

Spring Security 的 @PreFilter 注解能够很好地解决这个问题。与 @PostFilter 非常类似，@PreFilter 也使用 SpEL 来过滤集合， 只有满足 SpEL 表达式的元素才会留在集合中。但是它所过滤的不是方法的返回值，@PreFilter 过滤的是要进入方法中的集合成员。

@PreFilter 的使用非常简单。如下的 deleteSpittles() 方法使用了 @PreFilter 注解：

```java
@PreAuthorize("hasRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PostFilter("hasRole('ROLE_ADMIN') || filterObject.spitter.username == principal.username")
public void deleteSpittles(List<Spittle> spittles) { ... }
```

与前面一样，对于没有 ROLE\_SPITTER 或 ROLE\_ADMIN 权限的用户，@PreAuthorize 注解会阻止对这个方法的调用。但同时，@PreFilter 注解能够保证传递给 deleteSpittles() 方法的列表中，只包含当前用户有权限删除的 Spittle。这个表达式会针对集合中的每个元素进行计算，只有表达式计算结果为 true 的元素才会保留在列表中。targetObject 是 Spring Security 提供的另外一个值，它代表了要进行计算的当前列表元素。

Spring Security 提供了注解驱动的功能，这是通过一系列注解来实现的，到此为止，我们已经对这些注解进行了介绍。相对于判断用户所授予的权限，使用表达式来定义安全限制是一种更为强大的方式。

即便如此，我们也不应该让表达式过于聪明智能。我们应该避免编写非常复杂的安全表达式，或者在表达式中嵌入太多与安全无关的业务逻辑。而且，表达式最终只是一个设置给注解的 String 值，因此它很难测试和调试。

如果你觉得自己的安全表达式难以控制了，那么就应该看一下如何编写自定义的许可计算器（permission evaluator），以简化你的 SpEL 表达式。下面我们看一下如何编写自定义的许可计算器，用它来简化之前用于过滤的表达式。

**定义许可计算器**

我们在 @PreFilter 和 @PostFilter 中所使用的表达式还算不上太复杂。但是，它也并不简单，我们可以很容易地想象如果还要实现其他的安全规则，这个表达式会不断膨胀。在变得很长之前，表达式就会笨重、复杂且难以测试。

其实我们能够将整个表达式替换为更加简单的版本，如下所示：现在，设置给 @PreFilter 的表达式更加紧凑。它实际上只是在问一个问题 “用户有权限删除目标对象吗？”。如果有的话，表达式的计算结果为 true，Spittle 会保存在列表中，并传递给 deleteSpittles() 方法。如果没有权限的话，它将会被移除掉。

但是，hasPermission() 是哪来的呢？它的意思是什么？更为重要的是，它如何知道用户有没有权限删除 targetObject 所对应的 Spittle 呢？

hasPermission() 函数是 Spring Security 为 SpEL 提供的扩展，它为开发者提供了一个时机，能够在执行计算的时候插入任意的逻辑。我 们所需要做的就是编写并注册一个自定义的许可计算器。程序清单 14.1 展现了 SpittlePermissionEvaluator 类，它就是一个自定义的许可计算器，包含了表达式逻辑。

```java
package spittr.security;

import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import spittr.Spittle;

public class SpittlePermissionEvaluator implements PermissionEvaluator {
  private static final GrantedAuthority ADMIN_AUTHORITY =
    new GrantedAuthoritylmpl("ROLE_ADMIN");
  public boolean hasPermission(Authentication authentication, Object target, Object permission) {
    
    if (target instanceof Spittle) {
      Spittle spittle = (Spittle) target;
      String username = spittle.getSpitter().getUsername();
      if ("delete".equals(permission)) {
        return isAdmin(authentication) || username.equals(authentication.getName());
      }
    }
    
    throw new UnsupportedOperationException(
      "hasPermission not supported for object <" + target
      + "> and permission <" + permission + ">");
  }
  
  public boolean hasPermission(Authentication authentication, Serializable targetId,
      String targetType, Object permission) {
      
      throw new UnsupportedOperationException();
  }
  
  private boolean isAdmin(Authentication authentication) {
    return authentication.getAuthorities().contains(ADMIN_AUTHORITY);
  }
}
```

SpittlePermissionEvaluator 实现了 Spring Security 的 PermissionEvaluator 接口，它需要实现两个不同的 hasPermission() 方法。其中的一个 hasPermission() 方法把要评估的对象作为第二个参数。第二个 hasPermission() 方法在只有目标对象的 ID 可以得到的时候才有用，并将 ID 作为 Serializable 传入第二个参数。

为了满足我们的需求，我们假设使用 Spittle 对象来评估权限，所以第二个方法只是简单地抛出 UnsupportedOperationException。

对于第一个 hasPermission() 方法，要检查所评估的对象是否为一个 Spittle，并判断所检查的是否为删除权限。如果是这样，它将对比 Spitter 的用户名是否与认证用户的名称相等，或者当前用户是否具有 ROLE\_ADMIN 权限。

许可计算器已经准备就绪，接下来需要将其注册到 Spring Security 中，以便在使用 @PreFilter 表达式的时候支持 hasPermission() 操作。为了实现该功能，我们需要替换原有的表达式处理器，换成使用自定义许可计算器的处理器。

默认情况下，Spring Security 会配置为使用 DefaultMethodSecurityExpressionHandler，它会使用一个 DenyAllPermissionEvaluator 实例。顾名思义，DenyAllPermissionEvaluator 将会在 hasPermission() 方法中始终返回 false，拒绝所有的方法访问。但是，我们可以为 Spring Security 提供另外一个 DefaultMethodSecurityExpressionHandler，让它使用我们自定义的 SpittlePermissionEvaluator，这需要重载 GlobalMethodSecurityConfiguration 的 createExpressionHandler 方法：

```java
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
  DefaultMethodSecurityExpressionHandler expressionHandler =
    new DefaultMethodSecurityExpressionHandler();
  expressionHandler.setPermissionEvaluator(new SpittlePermissionEvaluator());
  return expressionHandler;
}
```

现在，我们不管在任何地方的表达式中使用 hasPermission() 来保护方法，都会调用 SpittlePermissionEvaluator 来决定用户是否有权限调用方法。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v4/untitled-8/untitled-1/untitled.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
