【问题标题】:Java Spring Security @PostFilter performanceJava Spring Security @PostFilter 性能
【发布时间】:2021-09-10 17:13:24
【问题描述】:

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

    @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;
    }

【问题讨论】:

  • 这些对象多久更改一次?
  • @SimonMartinelli 这些是用户可编辑的对象,因此可以随时更改
  • 您可能需要在存储库中移动 hasPermission(...) 以对其进行优化,如果有帮助,请查看此内容:stackoverflow.com/questions/49058363/…
  • 并在将权限检查条件移入查询时使用存储库中的 Pageable 方法

标签: java spring spring-boot spring-security


【解决方案1】:

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

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

  • 要么使用Specifications,要么在数据库级别过滤结果,同时考虑Spring Security SecurityContext暴露的主体信息,包括必要的过滤器Predicates,以限制返回的信息。
  • 或者,如果您使用的是 Hibernate,请再次使用entity filters,根据 Spring Security 公开的有关主体的信息,应用必要的数据限制。请参阅this related SO question,它提供了有关解决方案的详细信息。

请考虑 Spring Data Specifications 的用例。

假设我们正在处理银行帐户,而不是 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> {
}

为了根据执行查询的用户过滤信息,我们可以定义一个实用方法,该方法根据用户权限返回PredicatesList,我们可以稍后将其添加到那些我们在过滤银行账户时使用了某个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;
}

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

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 在他的评论中提出的解决方案看起来也很有趣,尽管老实说我从未测试过。

【讨论】:

  • 嗨@jccampanero,感谢您的回答,但我发现很难理解您提出的解决方案。举几个例子可能会有所帮助。
  • 嗨@geschema。不客气。当然,请查看更新的答案,希望对您有所帮助。如果您需要进一步说明,请告诉我。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-23
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 2020-12-16
相关资源
最近更新 更多