Java Spring 安全@PostFilter 性能

Java Spring Security @PostFilter performance

我的 Java Spring 应用程序使用 ACL,它有一个服务方法来检索与给定用户对应的所有对象:

    @Override
    @PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMINISTRATION')")
    public List<SomeClass> findAll() {
        return SomeClassRepository.findAll();
    }

不幸的是,在数据库中有很多对象的情况下,此方法需要花费太多时间才能完成(超过 1 秒)。可能是因为它会先从数据库中取出所有对象,然后在内存中一个一个过滤。如何在不失去 Spring ACL 的优势的情况下优化它?

编辑:我现在想到的解决方案是为 acl_sidacl_entry 存储库创建存储库,并通过这些存储库获取感兴趣对象的 ID。与上述方法相比,这使我的执行时间缩短了 10 倍。新代码如下所示:

    @Override
    @PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMINISTRATION')")
    public List<SomeClass> findAll() {
        List<SomeClass> result = new ArrayList<SomeClass>();
        Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        AclSid sid = aclSidRepository.findBySid(Long.toString(userId));
        List<AclEntry> aclEntries = aclEntryRepository.findBySid(sid);
        for (AclEntry aclEntry : aclEntries) {
            AclObjectIdentity aclObjectIdentity = aclEntry.getAclObjectIdentity();
            AclClass aclClass = aclObjectIdentity.getObjectIdClass();
            if (aclClass.getClassName().equals("com.company.app.entity.SomeClass")) {
                Optional<SomeClass> SomeClass = SomeClassRepository
                        .findById(aclObjectIdentity.getObjectIdIdentity());
                if (SomeClass.isPresent()) {
                    result.add(SomeClass.get());
                }
            }
        }
        return result;
    }

由于Spring过滤内存中的信息性能将取决于实际结果的数量:如果有大量结果,恐怕在过滤信息之前只缓存您的存储库结果可能是一个合适的解决方案。

要处理该问题,您可以在数据库级别过滤结果。我想到了两种方法:

  • 要么使用 Specifications,并在数据库级别过滤结果,同时考虑 Spring 安全性 SecurityContext 公开的有关主体的信息,并包括必要的过滤器 Predicates为了限制返回的信息。
  • 或者,如果您使用的是 Hibernate,请使用 entity filters to again, based on the information about the principal exposed by Spring Security, apply the necessary data restrictions. Please, see ,它提供了有关解决方案的详细信息。

请考虑 Spring 数据 Specification 的用例。

而不是 SomeClass,让我们假设我们正在处理银行账户。让我们创建相应的实体:

@Entity
public class BankAccount {

  @Id
  private String accountNumber;
  private Float balance;
  private String owner;
  private String accountingDepartment;

  //...

}

以及对应的仓库:

public interface BankAccountRepository extends Repository<BankAccount, String>, JpaSpecificationExecutor<BankAccount, String> {
}

为了根据执行查询的用户过滤信息,我们可以定义一个实用方法,根据用户权限,returns List of Predicates 我们可以稍后添加到我们在过滤银行账户时在某个 Specification 中使用的账户:

public static List<Predicate> getPredicatesForRestrictingDataByUser(Root<BankAccount> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
  // I realized in your edit that you are returning the user id instead of the user object.
  // There is nothing wrong with it but you are losing a valuable information: if you provide
  // a convenient UserDetails implementation you can have direct access to the authorities a user has, etc
  User user = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

  // Restrict data based on actual permissions

  // If the user is an admin, we assume that he/she can see everything, and we will no return any predicates
  if (hasAuthority(user, 'ADMINISTRATION')) {
    return Collections.emptyList();
  }

  // Let's introduce the accounting manager role.
  // Suppose that an accounting manager can see all the accounts in his/her department
  if (hasAuthority(user, 'ACCOUNTING_MANAGER')) {
    return Collections.singletonList(cb.equal(root.get(BankAccount_.accountingDeparment), user.getDepartment()))
  }

  // In any other case, a user can only see the bank account if he/she is the account owner
  return Collections.singletonList(cb.equal(root.get(BankAccount_.owner), user.getId()));
}

其中 hasAuthority 可以看起来像:

public static boolean hasAuthority(User user, String... authorities) {
  if (user instanceof UserDetails) {
    for (String authority : authorities) {
      return authentication.getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .findAny(a -> a.equals(authority))
        .isPresent();
    }
  }

  return false;
}

现在,在构建您的 Specification 时使用这些方法。考虑例如:

public static Specification<BankAccount> getBankAccounts(final BankAccountFilter filterCriteria) {
  return new Specification<BankAccount>() {

    @Override
    public Predicate toPredicate(Root<BankAccount> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
      List<Predicate> predicates = new ArrayList<Predicate>();

      // Build your predicate list according to the user provided filter criteria
      String accountNumber = filterCriteria.getAccountNumber();
      if (accountNumber != null) {
        predicates.add(cb.equal(root.get(BankAccount_.accountNmber), accountNumber);
      }

      //...

      // And now, restrict the information a user can see
      // Ideally, define getPredicatesForRestrictingDataByUser in a generic class more suitable for being reused
      List<Predicate> predicatesForRestrictingDataByUser = getPredicatesForRestrictingDataByUser(root, query, cb);
      predicates.addAll(predicatesForRestrictingDataByUser);

      Predicate predicate = cb.and(predicates.toArray(new Predicate[predicates.size()]));
      return predicate;
    }
  };
}

请原谅我的简单用例,但我希望你能理解。

@OsamaAbdulRehman 在他的评论中提出的解决方案看起来也很有趣,尽管老实说我从未测试过它。