diff --git a/springboot/fastbee-open-api/src/main/java/com/fastbee/data/acl/IotDataAccessGuard.java b/springboot/fastbee-open-api/src/main/java/com/fastbee/data/acl/IotDataAccessGuard.java new file mode 100644 index 00000000..fd48e7cb --- /dev/null +++ b/springboot/fastbee-open-api/src/main/java/com/fastbee/data/acl/IotDataAccessGuard.java @@ -0,0 +1,96 @@ +package com.fastbee.data.acl; + +import com.fastbee.common.core.domain.entity.SysUser; +import com.fastbee.common.core.domain.model.LoginUser; +import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.utils.SecurityUtils; +import com.fastbee.iot.domain.Device; +import com.fastbee.iot.domain.DeviceUser; +import com.fastbee.iot.domain.Product; +import com.fastbee.iot.mapper.DeviceMapper; +import com.fastbee.iot.mapper.DeviceUserMapper; +import com.fastbee.iot.mapper.ProductMapper; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Objects; + +/** + * 按 ID 直取单条记录的接口必须复用列表接口的归属范围。 + * 列表查询在 Service 层按角色注入 tenantId/userId,而详情、凭据、修改、删除接口此前只校验权限字符串, + * 配合自增主键可被遍历读取/操作他人数据。校验放在控制器边界:Service 方法同时被 MQTT broker + * 线程和内部流程调用,那里没有登录态。 + */ +@Component +public class IotDataAccessGuard { + + /* RuoYi 数据范围 1=全部数据权限,种子中 admin/manager/visitor 持有,保持其原有全局可见行为 */ + private static final String DATA_SCOPE_ALL = "1"; + + @Resource + private DeviceMapper deviceMapper; + @Resource + private DeviceUserMapper deviceUserMapper; + @Resource + private ProductMapper productMapper; + + public void assertDeviceAccessible(Long deviceId) { + checkDevice(deviceMapper.selectDeviceByDeviceId(deviceId), true); + } + + public void assertDeviceAccessibleBySerialNumber(String serialNumber) { + checkDevice(deviceMapper.selectDeviceBySerialNumber(serialNumber), true); + } + + /** 连接凭据可绕过 broker 侧 ACL 以设备身份接入,因此共享用户不发放,只给归属方 */ + public void assertDeviceConnectAccessible(Long deviceId) { + checkDevice(deviceMapper.selectDeviceByDeviceId(deviceId), false); + } + + public void assertProductAccessible(Long productId) { + Product product = productMapper.selectProductByProductId(productId); + if (product == null) { + return; + } + SysUser user = currentUser(); + if (isGlobal(user) || Objects.equals(product.getTenantId(), user.getUserId())) { + return; + } + throw new ServiceException("无权访问其他租户的产品"); + } + + private void checkDevice(Device device, boolean allowShare) { + if (device == null) { + return; + } + SysUser user = currentUser(); + if (isGlobal(user) + || Objects.equals(device.getUserId(), user.getUserId()) + || Objects.equals(device.getTenantId(), user.getUserId()) + || (allowShare && isShared(device.getDeviceId(), user.getUserId()))) { + return; + } + throw new ServiceException("无权访问其他用户的设备"); + } + + private boolean isShared(Long deviceId, Long userId) { + DeviceUser share = deviceUserMapper.selectDeviceUserByDeviceIdAndUserId(deviceId, userId); + return share != null && !"2".equals(share.getDelFlag()); + } + + private boolean isGlobal(SysUser user) { + if (user.isAdmin()) { + return true; + } + return user.getRoles() != null && user.getRoles().stream() + .anyMatch(role -> DATA_SCOPE_ALL.equals(role.getDataScope())); + } + + private SysUser currentUser() { + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser == null || loginUser.getUser() == null) { + throw new ServiceException("登录状态已失效,请重新登录"); + } + return loginUser.getUser(); + } +} diff --git a/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/DeviceController.java b/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/DeviceController.java index 5fa8404d..3ca45070 100644 --- a/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/DeviceController.java +++ b/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/DeviceController.java @@ -9,6 +9,7 @@ import com.fastbee.common.enums.BusinessType; import com.fastbee.common.utils.MessageUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.poi.ExcelUtil; +import com.fastbee.data.acl.IotDataAccessGuard; import com.fastbee.iot.domain.Device; import com.fastbee.iot.model.DeviceRelateUserInput; import com.fastbee.iot.model.ThingsModelItem.ThingsModel; @@ -46,6 +47,9 @@ public class DeviceController extends BaseController @Autowired private IDeviceService deviceService; + @Autowired + private IotDataAccessGuard accessGuard; + // @Lazy @Autowired private IMqttMessagePublish messagePublish; @@ -131,6 +135,7 @@ public class DeviceController extends BaseController @ApiOperation("获取设备详情") public AjaxResult getInfo(@PathVariable("deviceId") Long deviceId) { + accessGuard.assertDeviceAccessible(deviceId); return AjaxResult.success(deviceService.selectDeviceByDeviceId(deviceId)); } @@ -142,6 +147,7 @@ public class DeviceController extends BaseController @ApiOperation("设备数据同步") public AjaxResult deviceSynchronization(@PathVariable("serialNumber") String serialNumber) { + accessGuard.assertDeviceAccessibleBySerialNumber(serialNumber); return AjaxResult.success(messagePublish.deviceSynchronization(serialNumber)); } @@ -153,6 +159,7 @@ public class DeviceController extends BaseController @ApiOperation("根据设备编号获取设备详情") public AjaxResult getInfoBySerialNumber(@PathVariable("serialNumber") String serialNumber) { + accessGuard.assertDeviceAccessibleBySerialNumber(serialNumber); return AjaxResult.success(deviceService.selectDeviceBySerialNumber(serialNumber)); } @@ -175,6 +182,7 @@ public class DeviceController extends BaseController @ApiOperation("获取设备详情和运行状态") public AjaxResult getRunningStatusInfo(Long deviceId, Integer slaveId) { + accessGuard.assertDeviceAccessible(deviceId); return AjaxResult.success(deviceService.selectDeviceRunningStatusByDeviceId(deviceId,slaveId)); } @@ -217,6 +225,7 @@ public class DeviceController extends BaseController @ApiOperation("修改设备") public AjaxResult edit(@RequestBody Device device) { + accessGuard.assertDeviceAccessible(device.getDeviceId()); return deviceService.updateDevice(device); } @@ -229,6 +238,7 @@ public class DeviceController extends BaseController @ApiOperation("重置设备状态") public AjaxResult resetDeviceStatus(@PathVariable String serialNumber) { + accessGuard.assertDeviceAccessibleBySerialNumber(serialNumber); Device device=new Device(); device.setSerialNumber(serialNumber); return toAjax(deviceService.resetDeviceStatus(device.getSerialNumber())); @@ -242,6 +252,9 @@ public class DeviceController extends BaseController @DeleteMapping("/{deviceIds}") @ApiOperation("批量删除设备") public AjaxResult remove(@PathVariable Long[] deviceIds) throws SchedulerException { + for (Long deviceId : deviceIds) { + accessGuard.assertDeviceAccessible(deviceId); + } return toAjax(deviceService.deleteDeviceByDeviceId(deviceIds[0])); } @@ -259,6 +272,7 @@ public class DeviceController extends BaseController @GetMapping("/gwDevCount") @ApiOperation("子设备数量") public AjaxResult getGwDevCount(String gwDevCode){ + accessGuard.assertDeviceAccessibleBySerialNumber(gwDevCode); return AjaxResult.success(deviceService.getSubDeviceCount(gwDevCode)); } @@ -271,6 +285,7 @@ public class DeviceController extends BaseController @GetMapping("/getMqttConnectData") @ApiOperation("获取设备MQTT连接参数") public AjaxResult getMqttConnectData(Long deviceId){ + accessGuard.assertDeviceConnectAccessible(deviceId); return AjaxResult.success(deviceService.getMqttConnectData(deviceId)); } @@ -281,6 +296,7 @@ public class DeviceController extends BaseController @GetMapping("/listThingsModel") @ApiOperation("查询变量概况") public TableDataInfo listThingsModel(Integer pageNum, Integer pageSize, Long deviceId, String modelName, Integer type, Integer isMonitor, Integer isReadonly) { + accessGuard.assertDeviceAccessible(deviceId); Device device = deviceService.selectDeviceByDeviceId(deviceId); if (Objects.isNull(device)) { return new TableDataInfo(); diff --git a/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/ProductController.java b/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/ProductController.java index 08f82d55..924ab9ae 100644 --- a/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/ProductController.java +++ b/springboot/fastbee-open-api/src/main/java/com/fastbee/data/controller/ProductController.java @@ -6,6 +6,7 @@ import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.core.page.TableDataInfo; import com.fastbee.common.enums.BusinessType; import com.fastbee.common.utils.poi.ExcelUtil; +import com.fastbee.data.acl.IotDataAccessGuard; import com.fastbee.iot.domain.Product; import com.fastbee.iot.model.ChangeProductStatusModel; import com.fastbee.iot.model.IdAndName; @@ -33,6 +34,9 @@ public class ProductController extends BaseController @Autowired private IProductService productService; + @Autowired + private IotDataAccessGuard accessGuard; + /** * 查询产品列表 */ @@ -79,6 +83,7 @@ public class ProductController extends BaseController @ApiOperation("获取产品详情") public AjaxResult getInfo(@PathVariable("productId") Long productId) { + accessGuard.assertProductAccessible(productId); return AjaxResult.success(productService.selectProductByProductId(productId)); } @@ -103,6 +108,7 @@ public class ProductController extends BaseController @ApiOperation("修改产品") public AjaxResult edit(@RequestBody Product product) { + accessGuard.assertProductAccessible(product.getProductId()); return toAjax(productService.updateProduct(product)); } @@ -114,6 +120,7 @@ public class ProductController extends BaseController @ApiOperation("获取产品下面的设备数量") public AjaxResult deviceCount(@PathVariable Long productId) { + accessGuard.assertProductAccessible(productId); return AjaxResult.success(productService.selectDeviceCountByProductId(productId)); } @@ -126,6 +133,7 @@ public class ProductController extends BaseController @ApiOperation("更新产品状态") public AjaxResult changeProductStatus(@RequestBody ChangeProductStatusModel model) { + accessGuard.assertProductAccessible(model.getProductId()); return productService.changeProductStatus(model); } @@ -138,6 +146,9 @@ public class ProductController extends BaseController @ApiOperation("批量删除产品") public AjaxResult remove(@PathVariable Long[] productIds) { + for (Long productId : productIds) { + accessGuard.assertProductAccessible(productId); + } return productService.deleteProductByProductIds(productIds); }