上面的注解只能说明用户有“编辑客户”的功能权限,不能说明用户有权编辑 dto.id 对应的客户。后续仍然要看 Service 或 Mapper 是否加入了数据范围约束。
3. 追踪数据范围字段
常见的数据隔离字段包括:
tenant_id:租户隔离;
dept_id:部门隔离;
owner_id:数据负责人;
creator_id:创建人;
project_id:项目空间隔离;
org_path:组织树范围隔离。
如果业务表里存在这些字段,但查询时只按主键查,就需要重点关注。例如:
SELECT * FROM work_order WHERE id = #{id}
更合理的查询通常需要带上当前登录用户的数据范围:
SELECT * FROM work_order
WHERE id = #{id}
AND tenant_id = #{tenantId}
AND dept_id IN
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
这里不是说每个系统都必须按部门过滤,关键是权限边界要和业务模型一致。
关键修复方式
修复这类问题时,不建议只在 Controller 层临时判断。更稳妥的方式是把对象级权限沉到 Service 或 Repository 层,避免后续新增接口时再次遗漏。
1. 查询接口加入数据范围条件
原始代码:
public WorkOrderDetailVO getDetail(Long id) {
WorkOrder order = workOrderMapper.selectById(id);
if (order == null) {
throw new BizException("工单不存在");
}
return convert(order);
}
修复后示例:
public WorkOrderDetailVO getDetail(Long id) {
LoginUser user = SecurityContext.getLoginUser();
WorkOrder order = workOrderMapper.selectByIdAndScope(
id,
user.getTenantId(),
user.getDataScopeDeptIds()
);
if (order == null) {
throw new BizException("工单不存在或无权限访问");
}
return convert(order);
}
Mapper 示例:
SELECT * FROM work_order
WHERE id = #{id}
AND tenant_id = #{tenantId}
AND dept_id IN
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
LIMIT 1
public Boolean close(Long id) {
WorkOrder order = workOrderMapper.selectById(id);
if (order == null) {
throw new BizException("工单不存在");
}
order.setStatus("CLOSED");
return workOrderMapper.updateById(order) > 0;
}
即使前面查询加了权限判断,后面更新最好也带范围条件,减少并发或后续改动造成的风险:
public Boolean close(Long id) {
LoginUser user = SecurityContext.getLoginUser();
int rows = workOrderMapper.closeByIdAndScope(
id,
user.getTenantId(),
user.getDataScopeDeptIds()
);
if (rows == 0) {
throw new BizException("工单不存在或无权限操作");
}
return true;
}
UPDATE work_order
SET status = 'CLOSED', update_time = NOW()
WHERE id = #{id}
AND tenant_id = #{tenantId}
AND dept_id IN
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
3. 附件下载要绑定业务对象权限
附件接口是越权高发点。很多系统的文件表只记录了文件路径、文件名、上传人,却没有和业务权限绑定。
@GetMapping("/file/download")
public void download(@RequestParam Long fileId, HttpServletResponse response) {
fileService.download(fileId, response);
}
这里即使文件 ID 难猜,也不能作为安全边界。更合理的方式是:
文件表记录业务类型和业务 ID;
下载前根据业务类型找到对应权限校验器;
校验当前用户是否能访问该业务对象;
通过后再读取文件内容。
public void download(Long fileId, HttpServletResponse response) {
FileRecord file = fileMapper.selectById(fileId);
if (file == null) {
throw new BizException("文件不存在");
}
PermissionChecker checker = permissionCheckerFactory.get(file.getBizType());
if (!checker.canView(SecurityContext.getLoginUser(), file.getBizId())) {
throw new BizException("无权限下载该文件");
}
storageService.writeToResponse(file.getStorageKey(), response);
}
推荐意见