本文目录导读:

- 📖 目录导读
- 一、什么是数据权限,为什么它比功能权限更复杂?">一、什么是数据权限,为什么它比功能权限更复杂?
- 二、常见的数据权限实现方案对比">二、常见的数据权限实现方案对比
- 三、基于RBAC的数据权限扩展模型">三、基于RBAC的数据权限扩展模型
- 四、Java案例实战:基于MyBatis-Plus拦截器的行级权限实现">四、Java案例实战:基于MyBatis-Plus拦截器的行级权限实现
- 五、数据权限缓存与性能优化策略">五、数据权限缓存与性能优化策略
- 六、常见QA:关于数据权限的5个高频问题">六、常见QA:关于数据权限的5个高频问题
Java案例如何实现数据权限?从理论到实战的完整指南
📖 目录导读
- 什么是数据权限,为什么它比功能权限更复杂?
- 常见的数据权限实现方案对比
- 基于RBAC的数据权限扩展模型
- Java案例实战:基于MyBatis-Plus拦截器的行级权限实现
- 数据权限缓存与性能优化策略
- 常见QA:关于数据权限的5个高频问题
什么是数据权限,为什么它比功能权限更复杂?
场景还原:一个企业ERP系统中,销售经理A只能查看自己团队的订单,而财务总监B可以查看所有部门的订单,这就是典型的数据权限需求。
核心定义:数据权限控制的是“用户能看到哪些数据行”,而功能权限(如菜单、按钮)控制的是“用户能做什么操作”。
为什么难?
- 多维度交织:部门、职位、数据创建者、数据状态等都可能作为权限维度
- 动态性:用户归属可能变化,数据归属可能变化
- 性能压力:每条SQL都需附加权限过滤条件
问答环节
Q:为什么不直接在SQL里写死WHERE条件?
A:写死会导致业务SQL与权限逻辑强耦合,当权限规则变化时需修改所有相关SQL,维护成本极高。
常见的数据权限实现方案对比
| 方案 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| SQL拦截器 | 通过MyBatis拦截器自动拼接WHERE | 企业级SaaS、ERP | 无侵入、统一管理 | 需预定义权限字段 |
| 注解+切面 | 在方法上注解,AOP织入过滤条件 | 中等规模项目 | 灵活、可指定粒度 | 每个查询需注解,维护量略大 |
| 视图+数据库授权 | 创建不同数据库视图,授权给不同角色 | 报表类场景 | 数据库层隔离,性能好 | 视图灵活性差,扩展难度高 |
| 后端代码过滤 | 查询全量数据,在Java层面过滤 | 小数据量场景 | 实现简单 | 性能极差,大数据量不可用 |
推荐方案:对于80%的Java企业项目,SQL拦截器方案是最佳平衡点。
基于RBAC的数据权限扩展模型
经典的RBAC只解决了“用户-角色-权限”的映射,但未解决“数据行过滤”,我们需要扩展一个数据权限策略表:
-- 数据权限策略表
CREATE TABLE data_permission (
id BIGINT PRIMARY KEY,
role_id BIGINT, -- 角色ID
table_name VARCHAR(64), -- 应用的表名
field_name VARCHAR(64), -- 过滤字段名(如org_id, creator_id)
operator VARCHAR(8), -- 操作符(=, IN, like)
field_value VARCHAR(255), -- 过滤值(如部门ID, 用户ID)
strategy_type VARCHAR(32) -- 策略类型:SELF(本人) | DEPT(部门) | ALL(全部)
);
核心逻辑:
- 用户登录后,加载其所有角色的数据权限配置
- 当查询某表时,根据表名匹配策略
- 动态生成过滤条件并拼接至SQL
Java案例实战:基于MyBatis-Plus拦截器的行级权限实现
Step 1:定义数据权限上下文
public class DataPermissionContext {
private static final ThreadLocal<Long> currentOrgId = new ThreadLocal<>();
private static final ThreadLocal<List<Long>> accessibleOrgIds = new ThreadLocal<>();
public static void setAccessibleOrgIds(List<Long> ids) {
accessibleOrgIds.set(ids);
}
public static List<Long> getAccessibleOrgIds() {
return accessibleOrgIds.get();
}
public static void clear() {
currentOrgId.remove();
accessibleOrgIds.remove();
}
}
Step 2:登录时注入权限数据
// 用户登录后,根据角色查询可访问的组织ID List<Long> orgIds = dataPermissionService.getAccessibleOrgIds(userId); DataPermissionContext.setAccessibleOrgIds(orgIds);
Step 3:实现MyBatis-Plus的InnerInterceptor
@Component
public class DataPermissionInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
List<Long> accessibleOrgIds = DataPermissionContext.getAccessibleOrgIds();
if (accessibleOrgIds == null || accessibleOrgIds.isEmpty()) {
return;
}
// 获取原始SQL
String originalSql = boundSql.getSql();
// 动态拼接org_id权限条件
StringBuilder newSql = new StringBuilder(originalSql);
if (originalSql.contains("WHERE")) {
newSql.append(" AND org_id IN (");
} else {
newSql.append(" WHERE org_id IN (");
}
newSql.append(accessibleOrgIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(",")));
newSql.append(")");
try {
// 通过反射更新BoundSql中的SQL
Field field = BoundSql.class.getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql.toString());
} catch (Exception e) {
throw new RuntimeException("数据权限拦截失败", e);
}
}
}
Step 4:在MyBatis-Plus配置中注册
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataPermissionInterceptor()); // 需排在分页拦截器前
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
运行效果:开发人员只需编写普通查询,拦截器自动追加 WHERE org_id IN (1,2,3),且对所有使用MyBatis-Plus的查询生效。
数据权限缓存与性能优化策略
- 权限策略缓存:将用户的可访问组织ID缓存至Redis(
user_permission:{userId}),避免每次查询都读库 - SQL预编译优化:拦截器生成的SQL尽量使用
IN而不是OR,减少解析开销 - 分页兼容:拦截器必须放在
PaginationInnerInterceptor之前,否则分页总数统计会遗漏过滤条件 - 白名单机制:某些表(如字典表)无需数据权限,通过注解
@NoDataPermission跳过拦截
问答环节
Q:如果用户有多个角色,权限规则冲突怎么办?
A:采用“并集”策略——用户可访问所有角色允许的数据行,例如角色A能看到部门1,角色B能看到部门2,则用户能看到部门1和部门2。
常见QA:关于数据权限的5个高频问题
Q:数据权限会影响数据库索引吗?
A:会,建议在org_id、creator_id等过滤字段上建立联合索引,避免全表扫描。
Q:能用ShardingSphere做数据权限吗?
A:理论上可以,但ShardingSphere侧重于分库分表和读写分离,数据权限仍需自定义拦截器实现。
Q:如何处理跨表查询(JOIN)时的数据权限?
A:建议统一在查询的主表上设置权限过滤,如果主表是订单,过滤条件针对订单.org_id即可。
Q:微服务架构中数据权限如何处理?
A:推荐采用“权限下沉”策略——数据权限逻辑封装在基础服务中(如业务中台),对外API在参数中传递org_ids。
Q:测试时如何绕过数据权限?
A:提供开关或测试账号角色设置为“超级管理员”,其accessibleOrgIds不生效(可返回null跳过拦截)。
数据权限是Java企业项目从“能用”到“好用”的关键一步,本文提供的基于MyBatis-Plus拦截器方案已在多个SaaS平台验证,代码入侵低、扩展性强,建议生产项目中配合单元测试覆盖不同角色的查询结果,确保权限逻辑正确。