|
@@ -0,0 +1,173 @@
|
|
|
+package cn.iocoder.yudao.adminserver.framework.datapermission.core.rule;
|
|
|
+
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import cn.iocoder.yudao.adminserver.framework.datapermission.core.service.DeptDataPermissionService;
|
|
|
+import cn.iocoder.yudao.adminserver.framework.datapermission.core.service.dto.DeptDataPermissionRespDTO;
|
|
|
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
|
|
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
|
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
|
|
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
|
|
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
|
|
+import cn.iocoder.yudao.framework.security.core.LoginUser;
|
|
|
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import lombok.AllArgsConstructor;
|
|
|
+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.operators.conditional.OrExpression;
|
|
|
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
|
|
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
|
|
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
|
|
+
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
|
|
|
+ *
|
|
|
+ * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
|
|
|
+ *
|
|
|
+ * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
|
|
|
+ * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-admin-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
|
|
|
+public class DeptDataPermissionRule implements DataPermissionRule {
|
|
|
+
|
|
|
+ private static final String DEPT_COLUMN_NAME = "dept_id";
|
|
|
+ private static final String USER_COLUMN_NAME = "user_id";
|
|
|
+
|
|
|
+ private final DeptDataPermissionService deptDataPermissionService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于部门的表字段配置
|
|
|
+ * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
|
|
+ *
|
|
|
+ * key:表名
|
|
|
+ * value:字段名
|
|
|
+ */
|
|
|
+ private final Map<String, String> DEPT_TABLE_CONFIG = new HashMap<>();
|
|
|
+ /**
|
|
|
+ * 基于用户的表字段配置
|
|
|
+ * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
|
|
+ *
|
|
|
+ * key:表名
|
|
|
+ * value:字段名
|
|
|
+ */
|
|
|
+ private final Map<String, String> USER_TABLE_CONFIG = new HashMap<>();
|
|
|
+ /**
|
|
|
+ * 所有表名,是 {@link #DEPT_TABLE_CONFIG} 和 {@link #USER_TABLE_CONFIG} 的合集
|
|
|
+ */
|
|
|
+ private final Set<String> TABLE_NAMES = new HashSet<>();
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Set<String> getTableNames() {
|
|
|
+ return TABLE_NAMES;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Expression getExpression(String tableName, Alias tableAlias) {
|
|
|
+ // 只有有登陆用户的情况下,才进行数据权限的处理
|
|
|
+ LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
|
|
+ if (loginUser == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获得数据权限
|
|
|
+ DeptDataPermissionRespDTO deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser);
|
|
|
+ if (deptDataPermission == null) {
|
|
|
+ log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 情况一,如果是 ALL 可查看全部,则无需拼接条件
|
|
|
+ if (deptDataPermission.getAll()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
|
|
+ if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
|
|
+ && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
|
|
+ return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
|
|
+ }
|
|
|
+
|
|
|
+ // 情况三,拼接 Dept 和 User 的条件,最后组合
|
|
|
+ Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
|
|
|
+ Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
|
|
|
+ if (deptExpression == null && userExpression == null) {
|
|
|
+ log.error("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
|
|
|
+ JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
|
|
|
+ throw new NullPointerException(String.format("LoginUser(%d) tableName(%s) tableAlias(%s) 构建的条件为空",
|
|
|
+ loginUser.getId(), tableName, tableAlias.getName()));
|
|
|
+ }
|
|
|
+ if (deptExpression == null) {
|
|
|
+ return userExpression;
|
|
|
+ }
|
|
|
+ if (userExpression == null) {
|
|
|
+ return deptExpression;
|
|
|
+ }
|
|
|
+ // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ?
|
|
|
+ return new OrExpression(deptExpression, userExpression);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
|
|
|
+ // 如果不存在配置,则无需作为条件
|
|
|
+ String columnName = DEPT_TABLE_CONFIG.get(tableName);
|
|
|
+ if (StrUtil.isEmpty(columnName)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ // 拼接条件
|
|
|
+ return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
|
|
|
+ new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {
|
|
|
+ // 如果不查看自己,则无需作为条件
|
|
|
+ if (Boolean.FALSE.equals(self)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String columnName = USER_TABLE_CONFIG.get(tableName);
|
|
|
+ if (StrUtil.isEmpty(columnName)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ // 拼接条件
|
|
|
+ return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 添加配置 ====================
|
|
|
+
|
|
|
+ public void addDeptTableConfig(Class<? extends BaseDO> entityClass) {
|
|
|
+ addDeptTableConfig(entityClass, DEPT_COLUMN_NAME);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void addDeptTableConfig(Class<? extends BaseDO> entityClass, String columnName) {
|
|
|
+ String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
|
|
|
+ DEPT_TABLE_CONFIG.put(tableName, columnName);
|
|
|
+ TABLE_NAMES.add(tableName);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void addUserTableConfig(Class<? extends BaseDO> entityClass) {
|
|
|
+ addUserTableConfig(entityClass, DEPT_COLUMN_NAME);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void addUserTableConfig(Class<? extends BaseDO> entityClass, String columnName) {
|
|
|
+ String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
|
|
|
+ USER_TABLE_CONFIG.put(tableName, columnName);
|
|
|
+ TABLE_NAMES.add(tableName);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|