You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于多组用户实体关系,如何让@RepositoryRestResource与JpaRepository实现用户敏感?

嘿,这个场景我之前帮不少开发者处理过,结合你的实体关系,咱们可以从几个角度来实现Spring Data REST的用户敏感数据控制,一步步来:

方案1:用Spring Security注解+SpEL直接过滤(最直接)

首先得确保你已经集成了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}也会先校验权限再返回数据。

方案2:全局过滤器(适合多Repo统一控制)

如果你的项目里不止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的过滤只需要在配置里加对应的过滤器就行。

方案3:自定义Repository实现(复杂场景首选)

如果你的权限逻辑涉及更复杂的判断,比如要结合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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:41:57