# 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); | |
} | |
} |
# 基于部门的数据权限
项目实现了基于部门的数据权限,支持五种范围:
- 全部数据权限:无数据限制
- 指定部门数据权限:根据实际需要,设置可操作的部门
- 本部门数据权限:只能操作直接所属部门
- 本部门及以下数据:可以操作自己部门和以下的部门数据
- 仅本人数据权限:相对特殊,只能操作自己的数据
# 后台配置
在 系统管理->角色管理
下配置对应角色的数据权限
# 具体部门的数据权限实现:
/** | |
* <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(); | |
} | |
} | |
} |
# 具体流程:
实现 InnerInterceptor 类的
processSelect、processDelete、processUpdate
方法,使用自己的数据权限规则配置自己的数据规则权限
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;
}
实现
DataPermissionRule
配置自己的规则public class DeptDataPermissionRule implements DataPermissionRule {
}
在其他模块中使用
# 字段配置
每个 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"); | |
}; | |
} | |
} |
注意:
数据库的表必须添加以下字段(感觉这种方式耦合度高,或许可以用规则引擎优化一下)
- 基于【部门】过滤数据权限的表,需要添加部门编号字段,例如:
dept_id
- 基于【用户】过滤数据权限的表,需要添加用户编号字段,例如:
user_id
# @DataPermission 注解
@DataPermission
可声明在类和方法上,配置使用的数据权限规则。
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;
}
includeRules
属性,配置生效的DataPermissionRule
数据权限规则。列如说,项目里有 10 种 DataPermissionRule 规则,某个方法只想其中一种生效,则可以使用该属性。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); | |
} |
方法说明:
getTableNames()
方法:哪些数据库表需要使用该数据权限。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 |