# scaffold 项目之数据权限

# 数据权限

注意功能是指定用户,只能访问特定范围的数据,比如针对员工信息的数据控制。

普通员工 -> 访问自己的信息

部门领导 -> 所属部门的所有员工

...

# 实现:

上面的例子很简单,可以在代码中硬编码实现,就是根据不同的角色,判断查询不同的数据,这样虽然能实现,但是随着业务越迭代,类似的需求越多,那么代码的维护成本就越高

# 封装组件实现:

scaffold-spring-boot-starter-biz-data-permission 技术组件,实现的核心是每次对数据库操作时,他会自动拼接 WHERE data_column = ? 条件来进行过滤。

列如说,查看员工功能,对应的 sql 是 select * from system_users ,那么它拼接的结果是:

用户数据范围SQL
普通员工自己select * from system_users where id = 自己
部门员工部门下的所有员工select * from system_users where dept_id = 自己部门

# 具体实现:

知道上面核心内容,就可以进一步了解了,可以冲 Mybatis Plus 的 DataPermissionInterceptor 的一下三个方法入手:

  • #processSelect(...) 方法:处理 select 的 where 条件
  • #processUpdate(...) 方法:处理 select 的 where 条件
  • #processDelete(...) 方法:处理 select 的 where 条件
// 这个是 processUpdate 的具体实现,为 UPDATE 或 DELETE 语句获取需要的数据权限表达式, 
// 然后在设置或更新 PlainSelect 对象的 WHERE 条件,根据数据权限处理器的逻辑。
// 其他删除、查询类似
protected void processUpdate(Update update, int index, String sql, Object obj) {
    final Expression sqlSegment = getUpdateOrDeleteExpression(update.getTable(), update.getWhere(), (String) obj);
    if (null != sqlSegment) {
        update.setWhere(sqlSegment);
    }
}

# 基于部门的数据权限

项目实现了基于部门的数据权限,支持五种范围:

  1. 全部数据权限:无数据限制
  2. 指定部门数据权限:根据实际需要,设置可操作的部门
  3. 本部门数据权限:只能操作直接所属部门
  4. 本部门及以下数据:可以操作自己部门和以下的部门数据
  5. 仅本人数据权限:相对特殊,只能操作自己的数据

# 后台配置

系统管理->角色管理 下配置对应角色的数据权限

# 具体部门的数据权限实现:

/**
 * <p> Project: scaffold - DeptDataPermissionRule </p>
 *
 * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
 * <p>
 * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
 *
 * <p>
 * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
 * <li>
 *     1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【scaffold-server 采用该方案】
 * <li>
 *     2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
 *  <p>
 *      1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
 *      最终过滤条件是 WHERE dept_id = ?
 *  <p>
 *      2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
 *      最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
 *  <p>
 *      3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
 *      最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@AllArgsConstructor
@Slf4j
public class DeptDataPermissionRule implements DataPermissionRule {
    /**
     * LoginUser 的 Context 缓存 Key
     */
    protected static final String CONTEXT_KEY = DeptDataPermissionRule.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;
    /**
     * 基于部门的表字段配置
     * 一般情况下,每个表的部门编号字段是 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) {
        // 只有有登陆用户的情况下,才进行数据权限的处理
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        // 只有管理员类型的用户,才进行数据权限的处理
        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
            return null;
        }
        // 获得数据权限
        DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
        // 从上下文中拿不到,则调用逻辑进行获取
        if (deptDataPermission == null) {
            // 获取用户的部门数据权限
            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
            if (deptDataPermission == null) {
                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
                        loginUser.getId(), tableName, tableAlias.getName()));
            }
            // 添加到上下文中,避免重复计算
            loginUser.setContext(CONTEXT_KEY, deptDataPermission);
        }
        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
        if (deptDataPermission.getAll()) {
            return null;
        }
        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
        if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
            && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
            // WHERE null = null,可以保证返回的数据为空
            return new EqualsTo(null, null);
        }
        // 情况三,拼接 Dept 和 User 的条件,最后组合
        Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
        Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
        if (deptExpression == null && userExpression == null) {
            // TODO 获得不到条件的时候,暂时不抛出异常,而是不返回数据
            log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
//            throw new NullPointerException (String.format ("LoginUser (% d) Table (% s/% s) 构建的条件为空",
//                    loginUser.getId(), tableName, tableAlias.getName()));
            return EXPRESSION_NULL;
        }
        if (deptExpression == null) {
            return userExpression;
        }
        if (userExpression == null) {
            return deptExpression;
        }
        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
        return new Parenthesis(new OrExpression(deptExpression, userExpression));
    }
    private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
        // 如果不存在配置,则无需作为条件
        String columnName = deptColumns.get(tableName);
        if (StrUtil.isEmpty(columnName)) {
            return null;
        }
        // 如果为空,则无条件
        if (CollUtil.isEmpty(deptIds)) {
            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 = userColumns.get(tableName);
        if (StrUtil.isEmpty(columnName)) {
            return null;
        }
        // 拼接条件
        return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));
    }
    // ==================== 添加配置 ====================
    public void addDeptColumn(Class<? extends BaseDO> entityClass) {
        addDeptColumn(entityClass, DEPT_COLUMN_NAME);
    }
    public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
       addDeptColumn(tableName, columnName);
    }
    public void addDeptColumn(String tableName, String columnName) {
        deptColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }
    public void addUserColumn(Class<? extends BaseDO> entityClass) {
        addUserColumn(entityClass, USER_COLUMN_NAME);
    }
    public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
        addUserColumn(tableName, columnName);
    }
    public void addUserColumn(String tableName, String columnName) {
        userColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }
}

# 主要的实现方法:

/**
 * <p> Project: scaffold - DataPermissionDatabaseInterceptor </p>
 *
 * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
 * 主要的 SQL 重写方法,可见 {@link #builderExpression (Expression, List)} 方法
 * <p>
 * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。
 * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更!
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@RequiredArgsConstructor
public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor {
    private final DataPermissionRuleFactory ruleFactory;
    @Getter
    private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        //===== SELECT 场景 =====
        // 获得 Mapper 对应的数据权限的规则
        List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
        // 如果无需重写,则跳过
        if (mappedStatementCache.noRewritable(ms, rules)) {
            return;
        }
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        try {
            // 初始化上下文
            ContextHolder.init(rules);
            // 处理 SQL
            mpBs.sql(parserSingle(mpBs.sql(), null));
        } finally {
            // 添加是否需要重写的缓存
            addMappedStatementCache(ms);
            // 清空上下文
            ContextHolder.clear();
        }
    }
    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        //===== 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限) =====
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            // 获得 Mapper 对应的数据权限的规则
            List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
            // 如果无需重写,则跳过
            if (mappedStatementCache.noRewritable(ms, rules)) {
                return;
            }
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            try {
                // 初始化上下文
                ContextHolder.init(rules);
                // 处理 SQL
                mpBs.sql(parserMulti(mpBs.sql(), null));
            } finally {
                // 添加是否需要重写的缓存
                addMappedStatementCache(ms);
                // 清空上下文
                ContextHolder.clear();
            }
        }
    }
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        processSelectBody(select.getSelectBody());
        List<WithItem> withItemsList = select.getWithItemsList();
        if (!CollectionUtils.isEmpty(withItemsList)) {
            withItemsList.forEach(this::processSelectBody);
        }
    }
    /**
     * update 语句处理
     */
    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        final Table table = update.getTable();
        update.setWhere(this.builderExpression(update.getWhere(), table));
    }
    /**
     * delete 语句处理
     */
    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
    }
    // ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========
    protected void processSelectBody(SelectBody selectBody) {
        if (selectBody == null) {
            return;
        }
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            processSelectBody(withItem.getSubSelect().getSelectBody());
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = operationList.getSelects();
            if (CollectionUtils.isNotEmpty(selectBodyList)) {
                selectBodyList.forEach(this::processSelectBody);
            }
        }
    }
    /**
     * 处理 PlainSelect
     */
    protected void processPlainSelect(PlainSelect plainSelect) {
        //#3087 github
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (CollectionUtils.isNotEmpty(selectItems)) {
            selectItems.forEach(this::processSelectItem);
        }
        // 处理 where 中的子查询
        Expression where = plainSelect.getWhere();
        processWhereSubSelect(where);
        // 处理 fromItem
        FromItem fromItem = plainSelect.getFromItem();
        List<Table> list = processFromItem(fromItem);
        List<Table> mainTables = new ArrayList<>(list);
        // 处理 join
        List<Join> joins = plainSelect.getJoins();
        if (CollectionUtils.isNotEmpty(joins)) {
            mainTables = processJoins(mainTables, joins);
        }
        // 当有 mainTable 时,进行 where 条件追加
        if (CollectionUtils.isNotEmpty(mainTables)) {
            plainSelect.setWhere(builderExpression(where, mainTables));
        }
    }
    private List<Table> processFromItem(FromItem fromItem) {
        // 处理括号括起来的表达式
        while (fromItem instanceof ParenthesisFromItem) {
            fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
        }
        List<Table> mainTables = new ArrayList<>();
        // 无 join 时的处理逻辑
        if (fromItem instanceof Table) {
            Table fromTable = (Table) fromItem;
            mainTables.add(fromTable);
        } else if (fromItem instanceof SubJoin) {
            // SubJoin 类型则还需要添加上 where 条件
            List<Table> tables = processSubJoin((SubJoin) fromItem);
            mainTables.addAll(tables);
        } else {
            // 处理下 fromItem
            processOtherFromItem(fromItem);
        }
        return mainTables;
    }
    /**
     * 处理 where 条件内的子查询
     * <p>
     * 支持如下:
     * 1. in
     * 2. =
     * 3. >
     * 4. <
     * 5. >=
     * 6. <=
     * 7. <>
     * 8. EXISTS
     * 9. NOT EXISTS
     * <p>
     * 前提条件:
     * 1. 子查询必须放在小括号中
     * 2. 子查询一般放在比较操作符的右边
     *
     * @param where where 条件
     */
    protected void processWhereSubSelect(Expression where) {
        if (where == null) {
            return;
        }
        if (where instanceof FromItem) {
            processOtherFromItem((FromItem) where);
            return;
        }
        if (where.toString().indexOf("SELECT") > 0) {
            // 有子查询
            if (where instanceof BinaryExpression) {
                // 比较符号,and , or , 等等
                BinaryExpression expression = (BinaryExpression) where;
                processWhereSubSelect(expression.getLeftExpression());
                processWhereSubSelect(expression.getRightExpression());
            } else if (where instanceof InExpression) {
                // in
                InExpression expression = (InExpression) where;
                Expression inExpression = expression.getRightExpression();
                if (inExpression instanceof SubSelect) {
                    processSelectBody(((SubSelect) inExpression).getSelectBody());
                }
            } else if (where instanceof ExistsExpression) {
                // exists
                ExistsExpression expression = (ExistsExpression) where;
                processWhereSubSelect(expression.getRightExpression());
            } else if (where instanceof NotExpression) {
                // not exists
                NotExpression expression = (NotExpression) where;
                processWhereSubSelect(expression.getExpression());
            } else if (where instanceof Parenthesis) {
                Parenthesis expression = (Parenthesis) where;
                processWhereSubSelect(expression.getExpression());
            }
        }
    }
    protected void processSelectItem(SelectItem selectItem) {
        if (selectItem instanceof SelectExpressionItem) {
            SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
            if (selectExpressionItem.getExpression() instanceof SubSelect) {
                processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
            } else if (selectExpressionItem.getExpression() instanceof Function) {
                processFunction((Function) selectExpressionItem.getExpression());
            }
        }
    }
    /**
     * 处理函数
     * <p > 支持: 1. select fun (args..) 2. select fun1 (fun2 (args..),args..)<p>
     * <p> fixed gitee pulls/141</p>
     *
     * @param function {@link Function}
     */
    protected void processFunction(Function function) {
        ExpressionList parameters = function.getParameters();
        if (parameters != null) {
            parameters.getExpressions().forEach(expression -> {
                if (expression instanceof SubSelect) {
                    processSelectBody(((SubSelect) expression).getSelectBody());
                } else if (expression instanceof Function) {
                    processFunction((Function) expression);
                }
            });
        }
    }
    /**
     * 处理子查询等
     */
    protected void processOtherFromItem(FromItem fromItem) {
        // 去除括号
        while (fromItem instanceof ParenthesisFromItem) {
            fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
        }
        if (fromItem instanceof SubSelect) {
            SubSelect subSelect = (SubSelect) fromItem;
            if (subSelect.getSelectBody() != null) {
                processSelectBody(subSelect.getSelectBody());
            }
        } else if (fromItem instanceof ValuesList) {
            logger.debug("Perform a subQuery, if you do not give us feedback");
        } else if (fromItem instanceof LateralSubSelect) {
            LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
            if (lateralSubSelect.getSubSelect() != null) {
                SubSelect subSelect = lateralSubSelect.getSubSelect();
                if (subSelect.getSelectBody() != null) {
                    processSelectBody(subSelect.getSelectBody());
                }
            }
        }
    }
    /**
     * 处理 sub join
     *
     * @param subJoin subJoin
     * @return Table subJoin 中的主表
     */
    private List<Table> processSubJoin(SubJoin subJoin) {
        List<Table> mainTables = new ArrayList<>();
        if (subJoin.getJoinList() != null) {
            List<Table> list = processFromItem(subJoin.getLeft());
            mainTables.addAll(list);
            mainTables = processJoins(mainTables, subJoin.getJoinList());
        }
        return mainTables;
    }
    /**
     * 处理 joins
     *
     * @param mainTables 可以为 null
     * @param joins      join 集合
     * @return List<Table> 右连接查询的 Table 列表
     */
    private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {
        //join 表达式中最终的主表
        Table mainTable = null;
        // 当前 join 的左表
        Table leftTable = null;
        if (mainTables == null) {
            mainTables = new ArrayList<>();
        } else if (mainTables.size() == 1) {
            mainTable = mainTables.get(0);
            leftTable = mainTable;
        }
        // 对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
        Deque<List<Table>> onTableDeque = new LinkedList<>();
        for (Join join : joins) {
            // 处理 on 表达式
            FromItem joinItem = join.getRightItem();
            // 获取当前 join 的表,subJoint 可以看作是一张表
            List<Table> joinTables = null;
            if (joinItem instanceof Table) {
                joinTables = new ArrayList<>();
                joinTables.add((Table) joinItem);
            } else if (joinItem instanceof SubJoin) {
                joinTables = processSubJoin((SubJoin) joinItem);
            }
            if (joinTables != null) {
                // 如果是隐式内连接
                if (join.isSimple()) {
                    mainTables.addAll(joinTables);
                    continue;
                }
                // 当前表是否忽略
                Table joinTable = joinTables.get(0);
                List<Table> onTables = null;
                // 如果不要忽略,且是右连接,则记录下当前表
                if (join.isRight()) {
                    mainTable = joinTable;
                    if (leftTable != null) {
                        onTables = Collections.singletonList(leftTable);
                    }
                } else if (join.isLeft()) {
                    onTables = Collections.singletonList(joinTable);
                } else if (join.isInner()) {
                    if (mainTable == null) {
                        onTables = Collections.singletonList(joinTable);
                    } else {
                        onTables = Arrays.asList(mainTable, joinTable);
                    }
                    mainTable = null;
                }
                mainTables = new ArrayList<>();
                if (mainTable != null) {
                    mainTables.add(mainTable);
                }
                // 获取 join 尾缀的 on 表达式列表
                Collection<Expression> originOnExpressions = join.getOnExpressions();
                // 正常 join on 表达式只有一个,立刻处理
                if (originOnExpressions.size() == 1 && onTables != null) {
                    List<Expression> onExpressions = new LinkedList<>();
                    onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables));
                    join.setOnExpressions(onExpressions);
                    leftTable = joinTable;
                    continue;
                }
                // 表名压栈,忽略的表压入 null,以便后续不处理
                onTableDeque.push(onTables);
                // 尾缀多个 on 表达式的时候统一处理
                if (originOnExpressions.size() > 1) {
                    Collection<Expression> onExpressions = new LinkedList<>();
                    for (Expression originOnExpression : originOnExpressions) {
                        List<Table> currentTableList = onTableDeque.poll();
                        if (CollectionUtils.isEmpty(currentTableList)) {
                            onExpressions.add(originOnExpression);
                        } else {
                            onExpressions.add(builderExpression(originOnExpression, currentTableList));
                        }
                    }
                    join.setOnExpressions(onExpressions);
                }
                leftTable = joinTable;
            } else {
                processOtherFromItem(joinItem);
                leftTable = null;
            }
        }
        return mainTables;
    }
    // ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ==========
    /**
     * 处理条件
     *
     * @param currentExpression 当前 where 条件
     * @param table             单个表
     */
    protected Expression builderExpression(Expression currentExpression, Table table) {
        return this.builderExpression(currentExpression, Collections.singletonList(table));
    }
    /**
     * 处理条件
     *
     * @param currentExpression 当前 where 条件
     * @param tables 多个表
     */
    protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
        // 没有表需要处理直接返回
        if (CollectionUtils.isEmpty(tables)) {
            return currentExpression;
        }
        // 第一步,获得 Table 对应的数据权限条件
        Expression dataPermissionExpression = null;
        for (Table table : tables) {
            // 构建每个表的权限 Expression 条件
            Expression expression = buildDataPermissionExpression(table);
            if (expression == null) {
                continue;
            }
            // 合并到 dataPermissionExpression 中
            dataPermissionExpression = dataPermissionExpression == null ? expression
                    : new AndExpression(dataPermissionExpression, expression);
        }
        // 第二步,合并多个 Expression 条件
        if (dataPermissionExpression == null) {
            return currentExpression;
        }
        if (currentExpression == null) {
            return dataPermissionExpression;
        }
        // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
        if (currentExpression instanceof OrExpression) {
            return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
        }
        // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
        return new AndExpression(currentExpression, dataPermissionExpression);
    }
    /**
     * 构建指定表的数据权限的 Expression 过滤条件
     *
     * @param table 表
     * @return Expression 过滤条件
     */
    private Expression buildDataPermissionExpression(Table table) {
        // 生成条件
        Expression allExpression = null;
        for (DataPermissionRule rule : ContextHolder.getRules()) {
            // 判断表名是否匹配
            if (!rule.getTableNames().contains(table.getName())) {
                continue;
            }
            // 如果有匹配的规则,说明可重写。
            // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
            // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
            ContextHolder.setRewrite(true);
            // 单条规则的条件
            String tableName = MyBatisUtils.getTableName(table);
            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
            if (oneExpress == null){
                continue;
            }
            // 拼接到 allExpression 中
            allExpression = allExpression == null ? oneExpress
                    : new AndExpression(allExpression, oneExpress);
        }
        return allExpression;
    }
    /**
     * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中
     *
     * @param ms MappedStatement
     */
    private void addMappedStatementCache(MappedStatement ms) {
        if (ContextHolder.getRewrite()) {
            return;
        }
        // 无重写,进行添加
        mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules());
    }
    /**
     * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则
     *
     * @author 芋道源码
     */
    static final class ContextHolder {
        /**
         * 该 {@link MappedStatement} 对应的规则
         */
        private static final ThreadLocal<List<DataPermissionRule>> RULES = ThreadLocal.withInitial(Collections::emptyList);
        /**
         * SQL 是否进行重写
         */
        private static final ThreadLocal<Boolean> REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE);
        public static void init(List<DataPermissionRule> rules) {
            RULES.set(rules);
            REWRITE.set(false);
        }
        public static void clear() {
            RULES.remove();
            REWRITE.remove();
        }
        public static boolean getRewrite() {
            return REWRITE.get();
        }
        public static void setRewrite(boolean rewrite) {
            REWRITE.set(rewrite);
        }
        public static List<DataPermissionRule> getRules() {
            return RULES.get();
        }
    }
    /**
     * {@link MappedStatement} 缓存
     * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效
     * 如果无效,则可以避免 SQL 的解析,加快速度
     *
     * @author 芋道源码
     */
    static final class MappedStatementCache {
        /**
         * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效) 的缓存
         *
         * value:{@link MappedStatement#getId ()} 编号
         */
        @Getter
        private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>();
        /**
         * 判断是否无需重写
         * ps:虽然有点中文式英语,但是容易读懂即可
         *
         * @param ms MappedStatement
         * @param rules 数据权限规则数组
         * @return 是否无需重写
         */
        public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
            // 如果规则为空,说明无需重写
            if (CollUtil.isEmpty(rules)) {
                return true;
            }
            // 任一规则不在 noRewritableMap 中,则说明可能需要重写
            for (DataPermissionRule rule : rules) {
                Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
                if (!CollUtil.contains(mappedStatementIds, ms.getId())) {
                    return false;
                }
            }
            return true;
        }
        /**
         * 添加无需重写的 MappedStatement
         *
         * @param ms MappedStatement
         * @param rules 数据权限规则数组
         */
        public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
            for (DataPermissionRule rule : rules) {
                Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
                if (CollUtil.isNotEmpty(mappedStatementIds)) {
                    mappedStatementIds.add(ms.getId());
                } else {
                    noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId()));
                }
            }
        }
        /**
         * 清空缓存
         * 目前主要提供给单元测试
         */
        public void clear() {
            noRewritableMappedStatements.clear();
        }
    }
}

# 具体流程:

  1. 实现 InnerInterceptor 类的 processSelect、processDelete、processUpdate 方法,使用自己的数据权限规则

  2. 配置自己的数据规则权限

    protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
        // 没有表需要处理直接返回
        if (CollectionUtils.isEmpty(tables)) {
            return currentExpression;
        }
        // 第一步,获得 Table 对应的数据权限条件
        Expression dataPermissionExpression = null;
        for (Table table : tables) {
            // 构建每个表的权限 Expression 条件
            Expression expression = buildDataPermissionExpression(table);
            if (expression == null) {
                continue;
            }
            // 合并到 dataPermissionExpression 中
            dataPermissionExpression = dataPermissionExpression == null ? expression
                : new AndExpression(dataPermissionExpression, expression);
        }
        // 第二步,合并多个 Expression 条件
        if (dataPermissionExpression == null) {
            return currentExpression;
        }
        if (currentExpression == null) {
            return dataPermissionExpression;
        }
        // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
        if (currentExpression instanceof OrExpression) {
            return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
        }
        // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
        return new AndExpression(currentExpression, dataPermissionExpression);
    }
    private Expression buildDataPermissionExpression(Table table) {
        // 生成条件
        Expression allExpression = null;
        for (DataPermissionRule rule : ContextHolder.getRules()) {
            // 判断表名是否匹配
            if (!rule.getTableNames().contains(table.getName())) {
                continue;
            }
            // 如果有匹配的规则,说明可重写。
            // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
            // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
            ContextHolder.setRewrite(true);
            // 单条规则的条件
            String tableName = MyBatisUtils.getTableName(table);
            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
            if (oneExpress == null){
                continue;
            }
            // 拼接到 allExpression 中
            allExpression = allExpression == null ? oneExpress
                : new AndExpression(allExpression, oneExpress);
        }
        return allExpression;
    }
  3. 实现 DataPermissionRule 配置自己的规则

    public class DeptDataPermissionRule implements DataPermissionRule {
    }
  4. 在其他模块中使用

# 字段配置

每个 Maven Model,通过自定义 DeptDataPermissionRuleCustomizer Bean,配置哪些表的哪些字段,进行数据权限的过滤。

/**
 * <p> Project: scaffold - DataPermissionConfiguration </p>
 *
 * system 模块的数据权限 Configuration
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Configuration(proxyBeanMethods = false)
public class DataPermissionConfiguration {
    @Bean
    public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() {
        return rule -> {
            // dept
            rule.addDeptColumn(AdminUserDO.class);
            rule.addDeptColumn(DeptDO.class, "id");
            // user
            rule.addUserColumn(AdminUserDO.class, "id");
        };
    }
}

注意:

数据库的表必须添加以下字段(感觉这种方式耦合度高,或许可以用规则引擎优化一下)

  1. 基于【部门】过滤数据权限的表,需要添加部门编号字段,例如: dept_id
  2. 基于【用户】过滤数据权限的表,需要添加用户编号字段,例如: user_id

# @DataPermission 注解

@DataPermission 可声明在类和方法上,配置使用的数据权限规则。

  1. enable 属性:当前类或方法是否使用数据权限控制,默认是使用状态,如需禁用 则设置为 false

    使用实例

    @DataPermission(enable = false)
    private static DataPermission getDisableDataPermissionDisable() {
        if (DATA_PERMISSION_DISABLE == null) {
            DATA_PERMISSION_DISABLE = DataPermissionUtils.class
                .getDeclaredMethod("getDisableDataPermissionDisable")
                .getAnnotation(DataPermission.class);
        }
        return DATA_PERMISSION_DISABLE;
    }
  2. includeRules 属性,配置生效的 DataPermissionRule 数据权限规则。列如说,项目里有 10 种 DataPermissionRule 规则,某个方法只想其中一种生效,则可以使用该属性。

  3. excludeRules 属性,配置排除的 DataPermissionRule 数据权限规则。列如说,项目里有 10 种 DataPermissionRule 规则,某个方法只想其中一种不生效,其他都生效,则可以使用该属性。

# 自定义权限规则

如果想实现自定义权限规则,则可以实现 DataPermissionRule 数据权限规则接口,并声明成 Spring Bean 即可。需要实现的只有两个方法:

/**
 * <p> Project: scaffold - DataPermissionRule </p>
 *
 * 数据权限规则接口
 * <p>
 * 通过实现接口,自定义数据规则。
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
public interface DataPermissionRule {
    /**
     * 返回需要生效的表名数组
     * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
     *
     * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo (Class)} 获得
     *
     * @return 表名数组
     */
    Set<String> getTableNames();
    /**
     * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
     *
     * @param tableName 表名
     * @param tableAlias 别名,可能为空
     * @return 过滤条件 Expression 表达式
     */
    Expression getExpression(String tableName, Alias tableAlias);
}

方法说明:

  1. getTableNames() 方法:哪些数据库表需要使用该数据权限。
  2. getExpression(...) 方法:当操作这些数据库表,要如何拼接 WHERE 条件。

# 封装组件

最后可以将数据权限功能封装成组件使用

package com.tz.scaffold.framework.datapermission.config;
import com.tz.scaffold.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
import com.tz.scaffold.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import com.tz.scaffold.framework.security.core.LoginUser;
import com.tz.scaffold.module.system.api.permission.PermissionApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
 * <p> Project: scaffold - ScaffoldDeptDataPermissionAutoConfiguration </p>
 *
 * 基于部门的数据权限 AutoConfiguration
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
public class ScaffoldDeptDataPermissionAutoConfiguration {
    @Bean
    public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
                                                         List<DeptDataPermissionRuleCustomizer> customizers) {
        // 创建 DeptDataPermissionRule 对象
        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
        // 补全表配置
        customizers.forEach(customizer -> customizer.customize(rule));
        return rule;
    }
}
package com.tz.scaffold.framework.datapermission.config;
import com.tz.scaffold.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
import com.tz.scaffold.framework.datapermission.core.db.DataPermissionDatabaseInterceptor;
import com.tz.scaffold.framework.datapermission.core.rule.DataPermissionRule;
import com.tz.scaffold.framework.datapermission.core.rule.DataPermissionRuleFactory;
import com.tz.scaffold.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
import com.tz.scaffold.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
 * <p> Project: scaffold - ScaffoldDataPermissionAutoConfiguration </p>
 *
 * 数据权限的自动配置类
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@AutoConfiguration
public class ScaffoldDataPermissionAutoConfiguration {
    @Bean
    public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
        return new DataPermissionRuleFactoryImpl(rules);
    }
    @Bean
    public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor,
                                                                               DataPermissionRuleFactory ruleFactory) {
        // 创建 DataPermissionDatabaseInterceptor 拦截器
        DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory);
        // 添加到 interceptor 中
        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
        MyBatisUtils.addInterceptor(interceptor, inner, 0);
        return inner;
    }
    @Bean
    public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
        return new DataPermissionAnnotationAdvisor();
    }
}

在 resource 目录下添加 MATE-INF/spring 目录,在目录下添加文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports 内容如下:

com.tz.scaffold.framework.datapermission.config.ScaffoldDataPermissionAutoConfiguration
com.tz.scaffold.framework.datapermission.config.ScaffoldDeptDataPermissionAutoConfiguration