java每日精进 5.08【框架之数据权限补充】
1.@DataPermission 注解
@DataPermission数据权限注解,可声明在类或者方法上,配置使用的数据权限规则。
① enable
属性:当前类或方法是否开启数据权限,默认是 true
开启状态,可设置 false
禁用状态。
也就是说,数据权限默认是开启的,无需添加 @DataPermission
注解
// UserProfileController.java@GetMapping("/get")
@Operation(summary = "获得登录用户信息")
@DataPermission(enable = false) // 关闭数据权限,避免只查看自己时,查询不到部门。
public CommonResult<UserProfileRespVO> profile() {// .. 省略代码if (user.getDeptId() != null) {DeptDO dept = deptService.getDept(user.getDeptId());resp.setDept(UserConvert.INSTANCE.convert02(dept));}// .. 省略代码
}
② includeRules
属性,配置生效的 DataPermissionRule (opens new window)数据权限规则。例如说,项目里有 10 种 DataPermissionRule 规则,某个方法只想其中的 1 种生效,则可以使用该属性。
③ excludeRules
属性,配置排除的 DataPermissionRule (opens new window)数据权限规则。例如说,项目里有 10 种 DataPermissionRule 规则,某个方法不想其中的 1 种生效,则可以使用该属性。
2.自定义数据权限规则
2.1 DataPermissionRule类
如果想要自定义数据权限规则,只需要实现DataPermissionRule (opens new window)数据权限规则接口,并声明成 Spring Bean 即可。需要实现的只有两个方法:
/*** 数据权限规则接口* 通过实现接口,自定义数据规则。例如说,*/
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);}
- DataPermissionRule 是一个接口,定义了数据权限规则的核心行为。它用于自定义数据权限逻辑,通过在 SQL 查询执行前动态添加 WHERE 或 OR 条件来限制用户访问的数据。实现该接口的类(如 DeptDataPermissionRule)需要指定哪些表需要权限控制,以及如何为这些表生成过滤条件。
Set<String> getTableNames()
- 作用:返回需要应用数据权限控制的表名集合
- 返回值:Set<String>,表示受权限控制的表名集合
- 说明:
- 数据权限基于 SQL 重写,系统需要知道哪些表需要添加权限过滤条件
- 表名通常通过 MyBatis Plus 的 TableInfoHelper.getTableInfo(Class) 从实体类获取
- 返回的表名集合用于 DataPermissionRuleHandler 判断当前查询的表是否需要权限控制
- 示例:
表示权限规则适用于 t_user 和 t_dept 表。@Override public Set<String> getTableNames() { return Set.of("t_user", "t_dept"); }
Expression getExpression(String tableName, Alias tableAlias)
- 作用:根据表名和别名生成 SQL 过滤条件(WHERE 或 OR 表达式)。
- 入参:
- tableName (String):查询的表名,例如 t_user。
- tableAlias (Alias):表的别名,可能为空(例如在多表查询中,表可能有别名如 u)。
- 返回值:Expression,表示 SQL 过滤条件,可能是 InExpression、EqualsTo 等 JSQLParser 表达式对象,或 null(表示无条件)。
- 说明:
- 该方法是权限规则的核心,负责根据业务逻辑生成限制条件的 SQL 表达式。
- 返回的 Expression 会被 MyBatis Plus 的 DataPermissionInterceptor 转换为 SQL 片段,追加到查询的 WHERE 子句。
- 如果返回 null,表示不添加任何权限条件(即允许访问所有数据)。
- 示例:
为 t_user 表生成条件 WHERE dept_id IN (1, 2)。@Override public Expression getExpression(String tableName, Alias tableAlias) {if ("t_user".equals(tableName)) {String column = tableAlias != null ? tableAlias.getName() + ".dept_id" : "dept_id";return new InExpression(new Column(column), new ExpressionList<>(new LongValue(1), new LongValue(2)));}return null; }
2.2 DeptDataPermissionRule 类
/*** 基于部门的 {@link DataPermissionRule} 数据权限规则实现** 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。** 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?* 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【moyun-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 (?, ?, ? ...)*/
@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<>();@Overridepublic Set<String> getTableNames() {return TABLE_NAMES;}@Overridepublic 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())) {return new EqualsTo(null, null); // WHERE 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 ParenthesedExpressionList(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),// Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号new ParenthesedExpressionList(new ExpressionList<LongValue>(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);}}
DeptDataPermissionRule 是 DataPermissionRule 接口的一个具体实现,专注于基于部门的权限控制。它通过表的 dept_id 和 user_id 字段生成过滤条件,支持以下权限逻辑:
- 用户可以查看指定部门(deptIds)的数据。
- 用户可以查看自己的数据(self)。
- 用户可以查看所有数据(all)。 该类还支持动态配置表和字段映射,解决部门变更时的权限问题(如是否修改历史数据的 dept_id)。
- Set<String> getTableNames()
- 作用:返回受权限控制的表名集合。
- 入参:无。
- 返回值:Set<String>,包含 TABLE_NAMES 中的表名。
- 说明:实现 DataPermissionRule 接口方法,返回 deptColumns 和 userColumns 中配置的表名。
- 示例:
deptColumns.put("t_user", "dept_id"); userColumns.put("t_user", "user_id"); // getTableNames() 返回 Set.of("t_user")
- Expression getExpression(String tableName, Alias tableAlias)
- 作用:为指定表生成权限过滤条件。
- 入参:
- tableName (String):表名,如 t_user。
- tableAlias (Alias):表别名,可能为 null。
- 返回值:Expression,SQL 过滤条件,或 null(无条件)。
- 逻辑:
- 检查登录用户是否存在(SecurityFrameworkUtils.getLoginUser),若无,返回 null。
- 检查用户是否为管理员(UserTypeEnum.ADMIN),若否,返回 null。
- 从 LoginUser 上下文获取权限(DeptDataPermissionRespDTO),若无则通过 permissionApi.getDeptDataPermission 获取并缓存。
- 根据权限处理:
- 若 all = true,返回 null(无条件,允许访问所有数据)。
- 若 deptIds 为空且 self = false,返回 EqualsTo(null, null)(确保无数据返回)。
- 否则,调用 buildDeptExpression 和 buildUserExpression 生成部门和用户条件。
- 组合条件:
- 若两者均为空,返回 EXPRESSION_NULL。
- 若仅一个非空,返回该条件。
- 若两者非空,使用 OR 组合(如 (dept_id IN (1, 2) OR user_id = 100))。
- 示例:
// 假设:tableName = "t_user", tableAlias = null // 用户:ID = 100, 权限:deptIds = [1, 2], self = true // deptColumns: {"t_user": "dept_id"} // userColumns: {"t_user": "user_id"} Expression expr = getExpression("t_user", null); // 返回:(dept_id IN (1, 2) OR user_id = 100)
- Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds)
- 作用:生成基于部门的过滤条件。
- 入参:
- tableName (String):表名。
- tableAlias (Alias):表别名。
- deptIds (Set<Long>):允许访问的部门 ID 集合。
- 返回值:Expression,部门过滤条件(如 dept_id IN (1, 2)),或 null(无条件)。
- 逻辑:
- 从 deptColumns 获取表对应的部门字段名,若无,返回 null。
- 若 deptIds 为空,返回 null。
- 构建 InExpression,列为 MyBatisUtils.buildColumn(tableName, tableAlias, columnName),值为 deptIds 转换成的 LongValue 列表。
- 示例:
// tableName = "t_user", tableAlias = null, deptIds = [1, 2] // deptColumns: {"t_user": "dept_id"} Expression expr = buildDeptExpression("t_user", null, Set.of(1L, 2L)); // 返回:dept_id IN (1, 2)
- Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId)
- 作用:生成基于用户的过滤条件。
- 入参:
- tableName (String):表名。
- tableAlias (Alias):表别名。
- self (Boolean):是否允许查看自己的数据。
- userId (Long):当前用户 ID。
- 返回值:Expression,用户过滤条件(如 user_id = 100),或 null(无条件)。
- 逻辑:
- 若 self = false,返回 null。
- 从 userColumns 获取表对应的用户字段名,若无,返回 null。
- 构建 EqualsTo,列为 MyBatisUtils.buildColumn(tableName, tableAlias, columnName),值为 LongValue(userId)。
- 示例:
// tableName = "t_user", tableAlias = null, self = true, userId = 100 // userColumns: {"t_user": "user_id"} Expression expr = buildUserExpression("t_user", null, true, 100L); // 返回:user_id = 100
- void addDeptColumn(Class<? extends BaseDO> entityClass)
- 作用:为实体类添加默认部门字段(dept_id)配置。
- 入参:
- entityClass (Class<? extends BaseDO>):实体类。
- 返回值:无。
- 说明:调用 addDeptColumn(entityClass, DEPT_COLUMN_NAME)。
- void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName)
- 作用:为实体类添加自定义部门字段配置。
- 入参:
- entityClass (Class<? extends BaseDO>):实体类。
- columnName (String):部门字段名。
- 返回值:无。
- 说明:通过 TableInfoHelper.getTableInfo 获取表名,调用 addDeptColumn(tableName, columnName)。
- void addDeptColumn(String tableName, String columnName)
- 作用:为指定表添加部门字段配置。
- 入参:
- tableName (String):表名。
- columnName (String):部门字段名。
- 返回值:无。
- 说明:将映射添加到 deptColumns,并将 tableName 添加到 TABLE_NAMES。
- 示例:
addDeptColumn("t_user", "dept_id"); // deptColumns: {"t_user": "dept_id"} // TABLE_NAMES: ["t_user"]
- void addUserColumn(Class<? extends BaseDO> entityClass)
- 作用:为实体类添加默认用户字段(user_id)配置。
- 入参:
- entityClass (Class<? extends BaseDO>):实体类。 “