Spring QueryDsl 分页过滤器按 ACL 权限

Posted

技术标签:

【中文标题】Spring QueryDsl 分页过滤器按 ACL 权限【英文标题】:Spring QueryDsl pagination filter by ACL permissions 【发布时间】:2020-07-03 06:49:18 【问题描述】:

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

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


应用程序在服务层中使用 访问控制列表 (ACL) 来检查单个资源的权限,例如使用 @PreAuthorize(hasPermission(#id, 'Team', 'READ')

我想允许用户请求他拥有读取权限的所有团队。我试着用 @PostFilter(hasPermission(filterObject, 'READ'),只要我使用Iterable&lt;Team&gt; 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 Security Reference Documentation建议使用支持分页的@Query编写自定义查询。

我如何编写这样一个复杂的查询,它支持QueryDsl 的谓词分页基于权限的过滤

2020 年 3 月 24 日接近

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

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

    ...

您如何使用自定义存储库来执行此操作,扩展 QueryDslRepositorySupport,以便检查权限的查询部分自动附加并隐藏在自定义存储库实现中?

【问题讨论】:

【参考方案1】:

基于此approach,我开发了一种可能性,它更像是一种肮脏的解决方法而不是解决方案。

该方法是向现有谓词添加额外的权限过滤器,例如由web support 生成的谓词。为此,必须首先将 ACL 表映射为 @Immutable JPA 实体,以便 QueryDsl 可以生成相应的 Q 类。

应附加 ACL 权限过滤器的此类谓词标有以下注释。

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 Module 生成并附加实际的过滤器查询。

@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()));
    

完整的源代码和配置请看这个 GitHub Gist。

【讨论】:

以上是关于Spring QueryDsl 分页过滤器按 ACL 权限的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data REST 的 QueryDSL 集成可以用来执行更复杂的查询吗?

jirutka/rsql-parser 和 QueryDSL

Spring数据查询dsl如何添加订单?

Spring Data REST 的 QueryDSL 集成,用于查询实体中集合映射的子属性

如何使用 QueryDSL 在 Spring Data JPA 中使用 order by 和 Limit

附加 QueryDSL 谓词或基于过滤器值构建谓词