java - Spring QueryDsl 分页过滤器按 ACL 权限
问题描述
让我们假设以下基于 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 Security 参考文档建议使用@Query
支持分页的自定义查询。
我如何编写这样一个复杂的查询,它支持QueryDsl 的 Predicate、Pagination和基于权限的过滤?
方法 03/24/20
在另一个论坛中,我遇到了以下基于 QueryDsl 的方法:ACL 表不是本地或自定义查询,而是映射为@Immutable
JPA 实体,因此生成 Q 类并使用它们手动过滤权限。
@Entity
@Immutable
@Table(name = "acl_object_identity")
public class AclObjectIdentity implements Serializable {
...
}
您如何使用自定义存储库来执行此操作,扩展QueryDslRepositorySupport
,以便检查权限的查询部分自动附加并隐藏在自定义存储库实现中?
解决方案
基于这种方法,我开发了一种比解决方案更肮脏的解决方法的可能性。
该方法是向现有谓词添加额外的权限过滤器,例如由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 模块生成并附加实际的过滤器查询。
@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。
推荐阅读
- python - 为什么我的代码一运行这些函数就会运行?Turtle 和 TKinter 问题
- ios - SwiftUI 根据自身高度设置圆角半径
- android - 无法在撰写导航中传递可打包对象
- twilio - 如何在 Twilio 中将贵公司和部门的名称显示为来电显示?
- c# - 通过 WCF 访问类库
- c# - Visual Studio 2019 错误 XA0136:运行方式命令失败,并显示“运行方式:包已损坏安装
- onedrive - mc onedrive 中的“重解析点缓冲区中存在的标签无效”
- android - intent.putExtra:以下函数都不能使用提供的参数调用
- python - Scapy 将监听 wlan0 但不会监听设置为监控模式的 wlan1
- javascript - 返回对象内所有数组的名称和id属性值