基于多组用户实体关系,如何让@RepositoryRestResource与JpaRepository实现用户敏感?
嘿,这个场景我之前帮不少开发者处理过,结合你的实体关系,咱们可以从几个角度来实现Spring Data REST的用户敏感数据控制,一步步来:
首先得确保你已经集成了Spring Security,能拿到当前登录的用户信息。咱们直接在GroupRepository上做文章,覆盖默认的查询方法,让它只返回当前用户有权限的群组:
@RepositoryRestResource public interface GroupRepository extends JpaRepository<Group, Long> { // 核心查询:只返回当前用户作为GroupMember所属的群组 @PreAuthorize("hasRole('USER')") @Query("SELECT g FROM Group g JOIN g.groupMembers gm WHERE gm.user.id = ?#{authentication.principal.id}") List<Group> findAllForCurrentUser(); // 覆盖默认的findAll,替换成用户敏感的版本 @Override @PreAuthorize("hasRole('USER')") default List<Group> findAll() { return findAllForCurrentUser(); } // 处理单条查询:验证用户是否有权访问该群组 @Override @PreAuthorize("hasRole('USER') and @groupSecurityService.hasAccess(authentication.principal.id, #id)") Optional<Group> findById(Long id); }
这里需要一个辅助的权限校验服务类GroupSecurityService,用来判断用户是否属于目标群组:
@Service public class GroupSecurityService { @Autowired private GroupMemberRepository groupMemberRepository; public boolean hasAccess(Long userId, Long groupId) { // 简单判断:用户是否是该群组的成员 return groupMemberRepository.existsByUserIdAndGroupId(userId, groupId); } }
这样Spring Data REST暴露的/groups端点就只会返回当前用户加入的群组,/groups/{id}也会先校验权限再返回数据。
如果你的项目里不止Group需要用户敏感过滤,那全局过滤器会更省心。咱们写一个RepositoryRestConfigurer的配置类,给所有相关Repository添加动态过滤逻辑:
@Configuration public class RestDataSecurityConfig implements RepositoryRestConfigurer { @Override public void addRepositoryFilters(RepositoryRestConfiguration config, FilterRegistrationBean<?> filterRegistrationBean) { // 给GroupRepository添加专属过滤器 config.getRepositoryFilters().add(currentUserGroupFilter()); } private Filter currentUserGroupFilter() { return new Filter() { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 跳过匿名用户 if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) { User currentUser = (User) auth.getPrincipal(); // 把当前用户ID放到请求上下文里,方便后续查询用 RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); if (attrs != null) { attrs.setAttribute("currentUserId", currentUser.getId(), RequestAttributes.SCOPE_REQUEST); } } chain.doFilter(request, response); } }; } }
然后在GroupRepository里利用这个上下文ID构建查询,记得要继承JpaSpecificationExecutor来支持动态查询:
@RepositoryRestResource public interface GroupRepository extends JpaRepository<Group, Long>, JpaSpecificationExecutor<Group> { @Override default List<Group> findAll() { Long currentUserId = (Long) RequestContextHolder.getRequestAttributes().getAttribute("currentUserId", RequestAttributes.SCOPE_REQUEST); // 构建动态查询条件:只返回当前用户所属的群组 Specification<Group> userFilter = (root, query, cb) -> { Join<Group, GroupMember> groupMemberJoin = root.join("groupMembers"); return cb.equal(groupMemberJoin.get("user").get("id"), currentUserId); }; return findAll(userFilter); } }
这个方案的好处是统一管理过滤逻辑,后续加其他Repo的过滤只需要在配置里加对应的过滤器就行。
如果你的权限逻辑涉及更复杂的判断,比如要结合GroupMemberItem的状态或者其他业务规则,那自定义Repository实现类会更灵活:
首先定义一个带自定义方法的接口:
@RepositoryRestResource public interface GroupRepository extends JpaRepository<Group, Long>, GroupRepositoryCustom { } // 自定义方法接口 public interface GroupRepositoryCustom { List<Group> findAllForCurrentUser(); }
然后实现这个自定义接口,在这里写你需要的复杂逻辑:
@Service public class GroupRepositoryImpl implements GroupRepositoryCustom { @Autowired private EntityManager em; @Autowired private Authentication authentication; @Override public List<Group> findAllForCurrentUser() { User currentUser = (User) authentication.getPrincipal(); // 这里可以写任意复杂的JPQL或者原生SQL String jpql = "SELECT g FROM Group g JOIN g.groupMembers gm WHERE gm.user.id = :userId"; return em.createQuery(jpql, Group.class) .setParameter("userId", currentUser.getId()) .getResultList(); } }
之后你可以选择覆盖默认的findAll方法,或者让Spring Data REST直接暴露findAllForCurrentUser这个自定义端点。
因为你的Group关联了GroupMember,GroupMember又关联了GroupMemberItem,默认情况下Spring Data REST会把这些关联数据也返回给用户,所以得确保这些关联数据也是用户敏感的。比如:
- 用
@Projection来定制返回的关联数据,只返回当前用户对应的GroupMember和GroupMemberItem:
@Projection(name = "userSensitive", types = Group.class) public interface UserSensitiveGroupProjection { Long getId(); String getName(); // 只返回当前用户的GroupMember记录 @Value("#{target.groupMembers.stream().filter(gm -> gm.user.id == authentication.principal.id).findFirst()}") GroupMember getCurrentGroupMember(); }
然后在GroupRepository上指定这个Projection作为默认返回:
@RepositoryRestResource(excerptProjection = UserSensitiveGroupProjection.class) public interface GroupRepository extends JpaRepository<Group, Long> { // ... }
这样用户拿到的群组数据里只会包含自己的GroupMember信息,不会看到其他用户的。
内容的提问来源于stack exchange,提问作者Stefan Falk

