如何禁止Spring Boot中Furniture与House的实体关联变更?
这个问题其实是双向OneToMany关联场景下常见的权限绕过坑——用户直接通过修改Furniture的house字段,把不属于自己权限范围的家具转移到自己有权限的House里,而绕过了原House的权限校验。结合你“无需检查每个关联实体权限,仅通过House权限控制”的需求,我推荐以下几个落地方案:
1. 核心方案:数据权限过滤,从根源上限制可访问的Furniture
最直接的思路是:让用户根本无法查询到不属于自己权限House下的Furniture,这样自然就无法进行后续的归属变更操作。用Spring Data JPA的动态过滤就能实现:
步骤1:实现Furniture的权限过滤Specification
创建一个Specification类,动态注入当前用户有权限的House ID列表,过滤出用户可访问的Furniture:
public class FurnitureSpecifications { public static Specification<Furniture> userAccessible() { // 获取当前登录用户 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); User currentUser = (User) auth.getPrincipal(); // 从你的权限服务中获取用户有权限的House ID集合 List<Long> allowedHouseIds = permissionService.getAllowedHouseIds(currentUser.getId()); // 构建过滤条件:仅返回所属House在允许列表中的Furniture return (root, query, cb) -> root.get("house").get("id").in(allowedHouseIds); } }
步骤2:在Repository中使用过滤条件
让你的FurnitureRepository继承JpaSpecificationExecutor,这样就能在查询时应用过滤规则:
@Repository public interface FurnitureRepository extends JpaRepository<Furniture, Long>, JpaSpecificationExecutor<Furniture> { }
步骤3:业务层统一查询逻辑
所有查询Furniture的操作都必须带上这个过滤条件,比如:
public Furniture getAuthorizedFurniture(Long furnitureId) { return furnitureRepository.findOne( FurnitureSpecifications.userAccessible() .and((root, query, cb) -> cb.equal(root.get("id"), furnitureId)) ) .orElseThrow(() -> new AccessDeniedException("Furniture不存在或你无权限操作")); }
这样,当用户尝试操作不属于自己权限的Furniture时,会直接抛出权限异常,根本无法进入后续的归属变更流程。
2. 控制关联变更入口:只通过House管理Furniture归属
不要暴露直接修改Furniture.house字段的API,而是统一通过House相关接口来管理关联关系,确保所有变更都经过House的权限校验:
示例:提供House级别的Furniture管理接口
比如暴露POST /houses/{houseId}/furnitures接口,用来向指定House添加Furniture,业务逻辑如下:
@Service public class HouseService { @Autowired private HouseRepository houseRepo; @Autowired private FurnitureRepository furnitureRepo; @Autowired private PermissionService permissionService; public void addFurnitureToHouse(Long houseId, Long furnitureId) { // 1. 先校验用户对目标House的读写权限(这一步你已经在做了) House targetHouse = houseRepo.findById(houseId) .orElseThrow(() -> new ResourceNotFoundException("House不存在")); if (!permissionService.hasWritePermission(targetHouse, getCurrentUser())) { throw new AccessDeniedException("你无权限修改该House"); } // 2. 获取用户有权限的Furniture(上面的过滤逻辑确保只能拿到自己的) Furniture furniture = getAuthorizedFurniture(furnitureId); // 3. 执行关联变更(如果是双向关联,记得维护双方关系) targetHouse.addFurniture(furniture); houseRepo.save(targetHouse); } private User getCurrentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return (User) auth.getPrincipal(); } }
同时,在House实体中维护双向关联的一致性:
@Entity public class House { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "house", cascade = CascadeType.ALL, orphanRemoval = true) private List<Furniture> furnitures = new ArrayList<>(); // 统一的关联维护方法,避免手动修改字段导致的不一致 public void addFurniture(Furniture furniture) { furnitures.add(furniture); furniture.setHouse(this); } // 移除关联的方法 public void removeFurniture(Furniture furniture) { furnitures.remove(furniture); furniture.setHouse(null); } // getters & setters }
3. 可选:JPA生命周期回调强化校验(补充方案)
如果担心还有漏网之鱼(比如某些地方直接调用了Furniture的save方法),可以在Furniture实体上添加生命周期回调,在更新前做最后一道校验:
@Entity public class Furniture { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne private House house; // 注入权限服务(注意:实体中注入服务需要Spring上下文支持,可通过@Configurable实现) @Autowired private transient PermissionService permissionService; @PreUpdate public void preUpdate() { User currentUser = getCurrentUser(); // 校验用户对当前House(变更前的House)和目标House(变更后的House)都有权限 // 这里结合数据过滤的话,其实变更前的House肯定是用户有权限的,所以主要校验目标House if (!permissionService.hasWritePermission(this.house, currentUser)) { throw new AccessDeniedException("你无权限修改该Furniture的归属"); } } private User getCurrentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return (User) auth.getPrincipal(); } // getters & setters }
总结
最符合你需求的组合方案是:数据权限过滤 + House级别的关联入口控制。前者从根源上限制了用户可操作的Furniture范围,后者确保所有关联变更都经过House的权限校验,完全不需要逐个检查关联实体的原所属House权限,就能彻底拦截非法的跨House转移操作。
内容的提问来源于stack exchange,提问作者slartidan

