Java实现数据权限案例

wen java案例 3

本文目录导读:

Java实现数据权限案例

  1. 为什么要做数据权限?— 不只是“登录”那么简单
  2. 核心概念拆解:数据权限 vs 功能权限
  3. 主流实现方案对比:SQL拼接、拦截器、注解驱动
  4. 案例实战:基于Spring Boot + MyBatis的行级数据权限
  5. 常见坑与性能优化建议
  6. 高频面试问答(Q&A)
  7. 总结与扩展阅读方向


从零到一:Java实现数据权限的实战指南(RBAC + 行级过滤 + 拦截器详解)**


目录导读

  1. 为什么要做数据权限?— 不只是“登录”那么简单
  2. 核心概念拆解:数据权限 vs 功能权限
  3. 主流实现方案对比:SQL拼接、拦截器、注解驱动
  4. 案例实战:基于Spring Boot + MyBatis的行级数据权限
    • 1 数据模型与表设计
    • 2 自定义注解 + AOP拦截器
    • 3 动态SQL拼接的核心逻辑
  5. 常见坑与性能优化建议
  6. 高频面试问答(Q&A)
  7. 总结与扩展阅读方向

为什么要做数据权限?— 不只是“登录”那么简单

在很多企业级系统中,功能权限(“你能不能访问这个页面”或“你能不能按这个按钮”)只解决了“能不能用”的问题,但真正的业务痛点在于数据权限

同样是“查看订单”菜单,销售A只能看到自己的订单,销售经理能看到整个团队的订单,而财务总监能看到公司所有订单。

如果只做功能权限,那么所有登录用户看到的都是同一份数据,这在多租户、多组织、多层级的企业系统中是致命的设计缺陷。

数据权限的本质:在SQL执行前或执行时,动态地给查询语句加上“行级过滤条件”,确保用户只能操作和查看自己被授权的数据行。


核心概念拆解:数据权限 vs 功能权限

维度 功能权限(菜单/按钮) 数据权限(行级别)
控制粒度 操作(如增删改查) 数据行(如订单记录)
实现方式 RBAC模型(角色-权限) 数据范围规则(本人、本部门、全部)
常见框架 Spring Security, Shiro 自定义拦截器 + SQL解析
冲突场景 有按钮但看不到数据 看得到菜单但数据为空

核心误区:很多新手以为用了Shiro或者Spring Security就自动有了数据权限,其实这两个框架默认只处理认证授权,不处理行级数据过滤,数据权限必须由开发者在DAO层或Service层自己实现。


主流实现方案对比:SQL拼接、拦截器、注解驱动

在Java生态中,实现数据权限主要有三种路径,各有优劣:

  1. 硬编码SQL拼接
    在每次查询时手动加 WHERE user_id = ?
    ✅ 简单直观;❌ 代码冗余,容易遗漏,维护成本极高。

  2. MyBatis拦截器(Interceptor)
    通过拦截Executorquery方法,解析SQL,动态追加条件。
    ✅ 统一、透明,改造量小;❌ 需要处理SQL解析(推荐JSqlParser),复杂SQL容易出错。

  3. 自定义注解 + AOP
    在Mapper或Service方法上打上@DataScope注解,通过AOP切面获取当前用户,通过线程变量传递参数,再注入到SQL中。
    ✅ 业务侵入性小,灵活;❌ 注解定义需要规范,且要处理方法嵌套。

本文实战采用方案2+3结合:用注解标记需要数据权限的方法,用拦截器统一处理。


案例实战:基于Spring Boot + MyBatis的行级数据权限

1 数据模型与表设计

假设我们有如下表:

  • sys_user(用户表):user_id, dept_id, user_name
  • sys_dept(部门表):dept_id, parent_id, dept_name
  • biz_order(业务订单表):order_id, order_no, creator_id, dept_id

数据权限规则定义(存入字典表或枚举):

  • 本人数据creator_id = 当前用户ID
  • 本部门数据dept_id = 当前用户部门ID
  • 本部门及以下数据dept_id IN (当前部门及所有子部门)
  • 全部数据:不追加条件

2 自定义注解 + AOP拦截器

定义一个注解,作用在Mapper或Service方法上:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
    // 指定表别名(如果SQL中有别名,必须对应)
    String tableAlias() default "";
    // 指定属于当前用户的列名,默认creator_id
    String userColumn() default "creator_id";
    // 指定部门列名,默认dept_id
    String deptColumn() default "dept_id";
}

使用MyBatis的Interceptor拦截器,在Executor执行前处理:

@Component
@Intercepts({
    @Signature(type = Executor.class, method = "query",
              args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataScopeInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        // 1. 判断当前方法是否拥有@DataScope注解
        // 2. 如果有,获取当前登录用户的上下文(从SecurityContext或ThreadLocal)
        // 3. 根据用户的角色(如普通员工、部门经理)计算出数据范围(如本人、本部门)
        // 4. 使用JSqlParser解析 BoundSql,拼接 where 条件
        // 5. 替换BoundSql后继续执行
        return invocation.proceed();
    }
}

核心代码逻辑(伪代码)

// 从用户上下文获取用户和部门
Long userId = SecurityUtils.getUserId();
Long deptId = SecurityUtils.getDeptId();
String roleKey = SecurityUtils.getRoleKey(); // 如 "manager"
// 解析原SQL
Select select = (Select) CCJSqlParserUtil.parse(boundSql.getSql());
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// 构建权限条件
StringBuilder condition = new StringBuilder();
if ("staff".equals(roleKey)) {
    condition.append(" creator_id = ").append(userId);
} else if ("manager".equals(roleKey)) {
    // 查询所有子部门ID
    List<Long> subDeptIds = deptService.getChildDeptIds(deptId);
    condition.append(" dept_id IN (").append(
        subDeptIds.stream().map(String::valueOf).collect(Collectors.joining(","))
    ).append(")");
}
// 如果原SQL已有where,则加上AND
plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), new CustomExpression(condition.toString())));

3 动态SQL拼接的核心细节

  • 注意表别名:如果SQL用了别名SELECT * FROM biz_order o,则拼接条件必须用o.creator_id
  • 避免全表扫描:数据权限条件尽量使用索引列(通常用dept_iduser_id)。
  • 多表关联:如果SQL是JOIN,要确保条件作用在正确的表上,防止误过滤。

常见坑与性能优化建议

坑1:SQL解析失败
复杂SQL(含子查询、UNION、窗口函数)可能被JSqlParser解析异常,建议:

  • 对所有涉及数据权限的SQL进行单元测试。
  • 若解析失败,降级为“全部数据”并记录告警日志,不要直接报错崩溃。

坑2:超级管理员绕过权限
一般做法是:如果当前用户是admin或具有数据全部角色,则跳过权限拦截,避免性能损耗。

坑3:分页插件和拦截器冲突
分页插件(如PageHelper)也实现Interceptor,注意拦截器执行顺序,建议让数据权限拦截器优先于分页插件执行(通过@Orderinterceptor.order),否则分页统计SQL可能漏掉数据集。

性能优化建议

  • 数据权限条件尽量写成IN (子查询),而不是先查子部门再拼接字符串(避免N+1问题)。
  • 对频繁查询的用户部门层级建立缓存(如Redis或本地Caffeine)。
  • 考虑在数据库层面建立部门层级表(递归CTE),交由SQL完成子部门递归。

高频面试问答(Q&A)

Q1:你们项目的数据权限具体是怎么实现的?
A:我们基于Spring Boot + MyBatis,定义了一个@DataScope注解,标记在需要拦截的Mapper方法上,通过MyBatis的Executor拦截器,在查询执行前,用JSqlParser解析SQL,根据当前登录用户的角色动态拼接WHERE条件,例如普通员工本人数据,部门经理本部门及以下数据,管理员则不加条件。

Q2:如果有一次查询关联了多张表,数据权限加在哪张表上?
A:这取决于业务语义,如果主表是订单,过滤条件加在主表;如果主表是客户,但要求只能看本部门的客户,则加在客户表,我们在注解上支持指定表别名和列名,以应对不同场景。

Q3:数据权限和Shiro/Spring Security有什么区别?
A:Shiro和Spring Security是功能权限框架,解决“用户能执行哪些操作”(比如能否访问/order/list接口),数据权限是SQL行级过滤,是业务逻辑层的数据隔离,两者互补,很多项目只用了框架,但没有实现行级数据权限,导致越权查看数据。

Q4:如果SQL特别复杂,JSqlParser解析失败怎么办?
A:这是常见风险,我们做了两件事:

  1. 数据库只允许规范的SQL写法,不写特别花哨的语句。
  2. 拦截器中catch解析异常,记录日志并返回全量数据(但会发送告警邮件给开发人员),这比直接500错误要安全。

Q5:数据权限会影响性能吗?如何优化?
A:会,因为多了一个WHERE条件,且可能涉及子查询(查部门层级),优化方案:

  • 部门表使用物化路径或通过Redis缓存层级关系。
  • 使用EXPLAIN分析执行计划,确保权限字段有索引。
  • 如果数据量极大,可采用“数据权限白名单”预先计算好用户可见的ID集合,但更推荐用数据库递归查询。

总结与扩展阅读方向

数据权限是Java后端开发中的高级需求,不是仅靠框架就能自动完成的,本文通过一个基于Spring Boot + MyBatis的实战案例,展示了注解标记 + 拦截器解析 + 动态SQL拼接的完整链路。

扩展方向

  • 集成spring-security,将角色与数据范围绑定。
  • 支持多租户场景(租户ID作为全局过滤)。
  • 使用MyBatis-PlusDataPermissionInterceptor,它是官方插件,但只支持简单的配置,复杂场景仍需自定义。
  • 探索基于Apache CalciteShardingSphere的更强大的SQL改写方案。

阅读建议:结合你自己的业务,先从“本部门数据”开始实践,因为这类条件最简单且最实用,不要一开始就搞复杂递归,否则容易陷入SQL解析的泥潭。

抱歉,评论功能暂时关闭!