|
@@ -0,0 +1,228 @@
|
|
|
+package cn.iocoder.yudao.framework.datapermission.core.rule.device;
|
|
|
+
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
|
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
|
|
+import cn.iocoder.yudao.framework.security.core.LoginUser;
|
|
|
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
|
|
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
|
|
|
+import lombok.AllArgsConstructor;
|
|
|
+import lombok.Getter;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import net.sf.jsqlparser.expression.Alias;
|
|
|
+import net.sf.jsqlparser.expression.Expression;
|
|
|
+import net.sf.jsqlparser.expression.LongValue;
|
|
|
+import net.sf.jsqlparser.expression.NullValue;
|
|
|
+import net.sf.jsqlparser.expression.operators.relational.*;
|
|
|
+import net.sf.jsqlparser.schema.Table;
|
|
|
+import net.sf.jsqlparser.statement.select.PlainSelect;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import net.sf.jsqlparser.schema.Column;
|
|
|
+
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
|
|
|
+ *
|
|
|
+ * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
|
|
|
+ *
|
|
|
+ * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
|
|
|
+ * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【yudao-server 采用该方案】
|
|
|
+ * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
|
|
|
+ * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
|
|
|
+ * 最终过滤条件是 WHERE dept_id = ?
|
|
|
+ * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
|
|
|
+ * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
|
|
|
+ * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
|
|
|
+ * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
|
|
|
+ *
|
|
|
+ * @author 芋道源码
|
|
|
+ */
|
|
|
+@AllArgsConstructor
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class DevicePersonDataPermissionRule implements DataPermissionRule {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * LoginUser 的 Context 缓存 Key
|
|
|
+ */
|
|
|
+ protected static final String CONTEXT_KEY = DevicePersonDataPermissionRule.class.getSimpleName();
|
|
|
+
|
|
|
+ private static final String DEPT_COLUMN_NAME = "dept_id";
|
|
|
+ private static final String USER_COLUMN_NAME = "user_id";
|
|
|
+
|
|
|
+ static final Expression EXPRESSION_NULL = new NullValue();
|
|
|
+
|
|
|
+ private final PermissionApi permissionApi;
|
|
|
+
|
|
|
+ // 存储表配置:key=表名, value=关联字段配置
|
|
|
+ private final Map<String, DeviceTableConfig> tableConfigs = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ private final DevicePersonService devicePersonService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于部门的表字段配置
|
|
|
+ * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
|
|
+ *
|
|
|
+ * key:表名
|
|
|
+ * value:字段名
|
|
|
+ */
|
|
|
+ private final Map<String, String> deptColumns = new HashMap<>();
|
|
|
+ /**
|
|
|
+ * 基于用户的表字段配置
|
|
|
+ * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
|
|
+ *
|
|
|
+ * key:表名
|
|
|
+ * value:字段名
|
|
|
+ */
|
|
|
+ private final Map<String, String> userColumns = new HashMap<>();
|
|
|
+ /**
|
|
|
+ * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集
|
|
|
+ */
|
|
|
+ private final Set<String> TABLE_NAMES = new HashSet<>();
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Set<String> getTableNames() {
|
|
|
+ return TABLE_NAMES;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Expression getExpression(String tableName, Alias tableAlias) {
|
|
|
+ // 1. 获取当前登录用户
|
|
|
+ LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
|
|
+ if (loginUser == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取表配置
|
|
|
+ DeviceTableConfig config = tableConfigs.get(tableName);
|
|
|
+ if (config == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 获取当前用户负责的设备ID集合
|
|
|
+ Set<Long> responsibleDeviceIds = getResponsibleDeviceIds(loginUser);
|
|
|
+
|
|
|
+ // 4. 构建设备责任人条件
|
|
|
+ Expression deviceExpression = buildDeviceExpression(config, tableAlias, responsibleDeviceIds);
|
|
|
+ if (deviceExpression == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 构建主表关联条件(EXISTS子查询)
|
|
|
+ return buildExistsExpression(config, tableAlias, deviceExpression);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<Long> getResponsibleDeviceIds(LoginUser loginUser) {
|
|
|
+ // 使用上下文缓存,避免重复查询
|
|
|
+ Set<Long> deviceIds = loginUser.getContext(CONTEXT_KEY, Set.class);
|
|
|
+ if (deviceIds == null) {
|
|
|
+ deviceIds = devicePersonService.getResponsibleDeviceIds(loginUser.getId());
|
|
|
+ loginUser.setContext(CONTEXT_KEY, deviceIds);
|
|
|
+ log.debug("[getResponsibleDeviceIds][用户({}) 负责的设备: {}]",
|
|
|
+ loginUser.getId(), JsonUtils.toJsonString(deviceIds));
|
|
|
+ }
|
|
|
+ return deviceIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Expression buildDeviceExpression(DeviceTableConfig config,
|
|
|
+ Alias tableAlias,
|
|
|
+ Set<Long> deviceIds) {
|
|
|
+ if (CollUtil.isEmpty(deviceIds)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建子表设备ID IN条件: device_id IN (?, ?, ...)
|
|
|
+ Column deviceColumn = new Column(tableAlias != null ?
|
|
|
+ tableAlias.getName() + "." + config.getDeviceColumn() :
|
|
|
+ config.getDeviceColumn());
|
|
|
+
|
|
|
+ ExpressionList<LongValue> valueList = new ExpressionList<>();
|
|
|
+ for (Long deviceId : deviceIds) {
|
|
|
+ valueList.add(new LongValue(deviceId));
|
|
|
+ }
|
|
|
+
|
|
|
+ return new InExpression(deviceColumn, valueList);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Expression buildExistsExpression(DeviceTableConfig config,
|
|
|
+ Alias tableAlias,
|
|
|
+ Expression deviceExpression) {
|
|
|
+ // 构建EXISTS子查询:
|
|
|
+ // EXISTS (SELECT 1 FROM 子表
|
|
|
+ // WHERE 子表.主表关联列 = 主表.主键列
|
|
|
+ // AND (设备条件))
|
|
|
+
|
|
|
+ // 1. 子查询表
|
|
|
+ Table subTable = new Table(config.getSubTable());
|
|
|
+ subTable.setAlias(new Alias("sub", false));
|
|
|
+
|
|
|
+ // 2. WHERE条件: 子表.主表关联列 = 主表.主键列
|
|
|
+ Column subTableJoinColumn = new Column("sub." + config.getSubTableJoinColumn());
|
|
|
+ Column mainTablePkColumn = new Column(
|
|
|
+ (tableAlias != null ? tableAlias.getName() + "." : "") + config.getMainTablePkColumn());
|
|
|
+ EqualsTo joinCondition = new EqualsTo(subTableJoinColumn, mainTablePkColumn);
|
|
|
+
|
|
|
+ // 3. 组合条件: 关联条件 AND 设备条件
|
|
|
+ ParenthesedExpressionList conditions = new ParenthesedExpressionList();
|
|
|
+ conditions.add(joinCondition);
|
|
|
+ conditions.add(deviceExpression);
|
|
|
+
|
|
|
+ // 4. 构建子查询
|
|
|
+ SubSelect subSelect = new SubSelect();
|
|
|
+ subSelect.setSelectBody(new PlainSelect()
|
|
|
+ .addSelectItems(new LongValue(1))
|
|
|
+ .withFromItem(subTable)
|
|
|
+ .withWhere(conditions));
|
|
|
+
|
|
|
+ return new ExistsExpression().withRightExpression(subSelect);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加设备责任人权限配置
|
|
|
+ *
|
|
|
+ * @param mainTable 主表名
|
|
|
+ * @param mainTablePkColumn 主表主键列名
|
|
|
+ * @param subTable 子表名
|
|
|
+ * @param subTableJoinColumn 子表关联主表的列名
|
|
|
+ * @param deviceColumn 设备ID列名
|
|
|
+ */
|
|
|
+ public void addConfig(String mainTable,
|
|
|
+ String mainTablePkColumn,
|
|
|
+ String subTable,
|
|
|
+ String subTableJoinColumn,
|
|
|
+ String deviceColumn) {
|
|
|
+ tableConfigs.put(mainTable, new DeviceTableConfig(
|
|
|
+ mainTablePkColumn, subTable, subTableJoinColumn, deviceColumn
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 表配置类
|
|
|
+ */
|
|
|
+ @Getter
|
|
|
+ @AllArgsConstructor
|
|
|
+ private static class DeviceTableConfig {
|
|
|
+ /**
|
|
|
+ * 主表主键列名
|
|
|
+ */
|
|
|
+ private final String mainTablePkColumn;
|
|
|
+ /**
|
|
|
+ * 子表名
|
|
|
+ */
|
|
|
+ private final String subTable;
|
|
|
+ /**
|
|
|
+ * 子表关联主表的列名
|
|
|
+ */
|
|
|
+ private final String subTableJoinColumn;
|
|
|
+ /**
|
|
|
+ * 设备ID列名
|
|
|
+ */
|
|
|
+ private final String deviceColumn;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|