spring-data-jpa Spring QueryDsl按ACL权限分页筛选器

r6vfmomb  于 2022-11-10  发布在  Spring
关注(0)|答案(1)|浏览(181)

让我们假设下面的基于Spring JPA的存储库支持QueryDsl。

@Repository
public interface TeamRepository extends JpaRepository<Team, Long>, QuerydslPredicateExecutor<Team> {

}

应用程序在服务层中使用 * 访问控制列表(ACL)* 来检查单个资源的权限,例如使用@PreAuthorize(hasPermission(#id, 'Team', 'READ')
我想允许一个用户请求所有他有读权限的团队。我试着使用@PostFilter(hasPermission(filterObject, 'READ'),只要我使用Iterable<Team> findAll(Predicate predicate)就能很好地工作。但是当我试着使用分页时,@PostFilter似乎抛出了一个异常。

java.lang.IllegalArgumentException: Filter target must be a collection, array, or stream type, but was Page 1 of 0 containing UNKNOWN instances

官方的Spring安全参考文档建议使用支持分页的@Query编写一个自定义查询。
如何编写这样一个支持QueryDsl的 predicate分页基于权限的过滤的复杂查询?

于2020年3月24日进场

在另一个forum中,我遇到了以下基于QueryDsl的方法:ACL tables被Map为@Immutable JPA实体,而不是本地或自定义查询,从而生成Q类,并使用它们手动过滤权限。

@Entity
@Immutable
@Table(name = "acl_object_identity")
public class AclObjectIdentity implements Serializable {

    ...
}

如何使用一个扩展了QueryDslRepositorySupport的自定义存储库来实现这一点,以便检查权限的查询部分被自动追加并隐藏在自定义存储库实现中?

s4chpxco

s4chpxco1#

基于 这个 approach , 我 开发 了 一 个 可能 性 , 它 与其 说 是 一 个 解决 方案 , 不如 说 是 一 个 肮脏 的 变通 方案 。
方法 是 向 现有 predicate 添加 一 个 额外 的 权限 过滤 器 , 例如 那些 由 web 支持 生成 的 predicate 。 为此 , ACL 表 必须 首先 Map 为 @Immutable JPA 实体 , 以便 QueryDsl 可以 生成 相应 的 Q 类 。
应 附加 ACL 权限 筛选 器 的 此类 predicate 标有 以下 注解 。

public Page<PostDTO> findAll(@QueryDslAclPermission(root = Post.class, permission = "READ") Predicate predicate, Pageable pageable) {

    ...
}

中 的 每 一 个
此 注解 主要 保存 有关 域 类型 的 元 信息 , 这些 信息 是 构建 筛选 器 查询 所 需 的 。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.TYPE})
public @interface QueryDslAclPermission {

    Class<?> root();

    String permission();

    String identifier() default "id";
}

格式
使用 以下 类 和 Spring 的 AOP 模块 生成 并 追加 实际 的 过滤 器 查询 。

@Aspect
@Component
public class QueryDslAclPermissionAspect {

    private PermissionFactory permissionFactory;

    @Autowired
    public QueryDslAclPermissionAspect(PermissionFactory permissionFactory) {
        this.permissionFactory = permissionFactory;
    }

    @Around(value = "execution(* *(.., @QueryDslAclPermission (*), ..))")
    public Object addPermissionFilter(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Parameter[] parameters = method.getParameters();
        Object[] arguments = joinPoint.getArgs();

        for(int index = 0; index < parameters.length; ++index) {

            if(parameters[index].getType().equals(Predicate.class) &&
                    parameters[index].isAnnotationPresent(QueryDslAclPermission.class)) {

                Predicate predicate = (Predicate) arguments[index];
                QueryDslAclPermission aclPermission = parameters[index].getAnnotation(QueryDslAclPermission.class);

                arguments[index] = addPermissionFilter(predicate, aclPermission);
            }
        }

        return joinPoint.proceed(arguments);
    }

    private Predicate addPermissionFilter(Predicate predicate, QueryDslAclPermission aclPermission) {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if(null == authentication || !authentication.isAuthenticated()) {
            throw new IllegalStateException("Permission filtering not possible for unauthenticated principal");
        }

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        PrincipalSid principalSid = new PrincipalSid(userDetails.getUsername());

        NumberPath<Long> idPath = new PathBuilderFactory().create(aclPermission.root())
                .getNumber(aclPermission.identifier(), Long.class);

        return idPath.in(selectPermitted(aclPermission.root(), principalSid,
                permissionFactory.buildFromName(aclPermission.permission()))).and(predicate);
    }

    private JPQLQuery<Long> selectPermitted(Class<?> targetType, PrincipalSid sid, Permission permission) {

        return selectAclEntry(targetType, sid, permission)
                .select(QAclEntry.aclEntry.aclObjectIdentity.objectIdIdentity);
    }

    private JPQLQuery<AclEntry> selectAclEntry(Class<?> targetType, PrincipalSid sid, Permission permission) {

        return new JPAQuery<AclEntry>().from(QAclEntry.aclEntry)
                .where(QAclEntry.aclEntry.aclObjectIdentity.id.in(selectAclObjectIdentity(targetType)
                        .select(QAclObjectIdentity.aclObjectIdentity.id))
                        .and(QAclEntry.aclEntry.aclSid.id.eq(selectAclSid(sid).select(QAclSid.aclSid.id)))
                        .and(QAclEntry.aclEntry.mask.eq(permission.getMask())));
    }

    private JPQLQuery<AclObjectIdentity> selectAclObjectIdentity(Class<?> targetType) {

        return new JPAQuery<AclObjectIdentity>().from(QAclObjectIdentity.aclObjectIdentity)
                .where(QAclObjectIdentity.aclObjectIdentity.objectIdClass.id.eq(selectAclClass(targetType)
                        .select(QAclClass.aclClass.id)));
    }

    private JPQLQuery<AclSid> selectAclSid(PrincipalSid sid) {

        return new JPAQuery<AclSid>().from(QAclSid.aclSid)
                .where(QAclSid.aclSid.sid.eq(sid.getPrincipal()));
    }

    private JPQLQuery<AclClass> selectAclClass(Class<?> targetType) {

        return new JPAQuery<AclClass>().from(QAclClass.aclClass)
                .where(QAclClass.aclClass.className.eq(targetType.getSimpleName()));
    }
}

格式

    • 2022 年 9 月 20 日 编辑 * *

在 我 的 GitHub Gist 中 可以 找到 一 种 基于 JPA 的 Specification<T> 和 自 定义 仓库 实现 的 更 通用 的 方法 。

相关问题