Java利用Mybatis进行数据权限控制

2023-11-08

权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:用户、角色、权限、资源。用户是源头,资源是目标,用户绑定至角色,资源与权限关联,最终将角色与权限关联,就形成了比较完整灵活的权限控制模型。
资源是最终需要控制的标的物,但是我们在一个业务系统中要将哪些元素作为待控制的资源呢?我将系统中待控制的资源分为三类:

  1. URL访问资源(接口以及网页)
  2. 界面元素资源(增删改查导入导出的按钮,重要的业务数据展示与否等)
  3. 数据资源

现在业内普遍的实现方案实际上很粗放,就是单纯的“菜单控制”,通过菜单显示与否来达到控制权限的目的。
我仔细分析过,现在大家做的平台分为To C和To B两种:

  1. To C一般不会有太多的复杂权限控制,甚至大部分连菜单控制都不用,全部都可以访问。
  2. To B一般都不是开放的,只要做好认证关口,能够进入系统的只有内部员工。大部分企业内部的员工互联网知识有限,而且作为内部员工不敢对系统进行破坏性的尝试。

所以针对现在的情况,考虑成本与产出,大部分设计者也不愿意在权限上进行太多的研发力量。
菜单和界面元素一般都是由前端编码配合存储数据实现,URL访问资源的控制也有一些框架比如SpringSecurity,Shiro。
目前我还没有找到过数据权限控制的框架或者方法,所以自己整理了一份。

数据权限控制原理

数据权限控制最终的效果是会要求在同一个数据请求方法中,根据不同的权限返回不同的数据集,而且无需并且不能由研发编码控制。这样大家的第一想法应该就是AOP,拦截所有的底层方法,加入过滤条件。这样的方式兼容性较强,但是复杂程度也会更高。我们这套系统中,采用的是利用Mybatis的plugin机制,在底层SQL解析时替换增加过滤条件。
这样一套控制机制存在很明显的优缺点,首先缺点:

  1. 适用性有限,基于底层的Mybatis。
  2. 方言有限,针对了某种数据库(我们使用Mysql),而且由于需要在底层解析处理条件所以有可能造成不同的数据库不能兼容。当然Redis和NoSQL也无法限制。

当然,假如你现在就用Mybatis,而且数据库使用的是Mysql,这方面就没有太大影响了。

接下来说说优点:

  1. 减少了接口数量及接口复杂度。原本针对不同的角色,可能会区分不同的接口或者在接口实现时利用流程控制逻辑来区分不同的条件。有了数据权限控制,代码中只用写基本逻辑,权限过滤由底层机制自动处理。
  2. 提高了数据权限控制的灵活性。例如原本只有主管能查本部门下组织架构/订单数据,现在新增助理角色,能够查询本部门下组织架构,不能查询订单。这样的话普通的写法就需要调整逻辑控制,使用数据权限控制的话,直接修改配置就好。

数据权限实现

上一节就提及了实现原理,是基于Mybatis的plugins(查看官方文档)实现。

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

Mybatis的插件机制目前比较出名的实现应该就是PageHelper项目了,在做这个实现的时候也参考了PageHelper项目的实现方式。所以权限控制插件的类命名为PermissionHelper。
机制是依托于Mybatis的plugins机制,实际SQL处理的时候基于jsqlparser这个包。
设计中包含两个类,一个是保存角色与权限的实体类命名为PermissionRule,一个是根据实体变更底层SQL语句的主体方法类PermissionHelper。

首先来看下PermissionRule的结构:

public class PermissionRule {

    private static final Log log = LogFactory.getLog(PermissionRule.class);
    /**
     * codeName<br>
     * 适用角色列表<br>
     * 格式如: ,RoleA,RoleB,
     */
    private String roles;
    /**
     * codeValue<br>
     * 主实体,多表联合
     * 格式如: ,SystemCode,User,
     */
    private String fromEntity;
    /**
     * codeDesc<br>
     * 过滤表达式字段, <br>
     * <code>{uid}</code>会自动替换为当前用户的userId<br>
     * <code>{me}</code> main entity 主实体名称
     * <code>{me.a}</code> main entity alias 主实体别名
     * 格式如:
     * <ul>
     * <li>userId = {uid}</li>
     * <li>(userId = {uid} AND authType > 3)</li>
     * <li>((userId = {uid} AND authType) > 3 OR (dept in (select dept from depts where manager.id = {uid})))</li>
     * </ul>
     */
    private String exps;

    /**
     * codeShowName<br>
     * 规则说明
     */
    private String ruleComment;

}

看完这个结构,基本能够理解设计的思路了。数据结构中保存如下几个字段:

  • 角色列表:需要使用此规则的角色,可以多个,使用英文逗号隔开。
  • 实体列表:对应的规则应用的实体(这里指的是表结构中的表名,可能你的实体是驼峰而数据库是蛇形,所以这里要放蛇形那个),可以多个,使用英文逗号隔开。
  • 表达式:表达式就是数据权限控制的核心了。简单的说这里的表达式就是一段SQL语句,其中设置了一些可替换值,底层会用对应运行时的变量替换对应内容,从而达到增加条件的效果。
  • 规则说明:单纯的一个说明字段。

核心流程
系统启动时,首先从数据库加载出所有的规则。底层利用插件机制来拦截所有的查询语句,进入查询拦截方法后,首先根据当前用户的权限列表筛选出PermissionRule列表,然后循环列表中的规则,对语句中符合实体列表的表进行条件增加,最终生成处理后的SQL语句,退出拦截器,Mybatis执行处理后SQL并返回结果。

讲完PermissionRule,再来看看PermissionHelper,首先是头:

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PermissionHelper implements Interceptor {
}

头部只是标准的Mybatis拦截器写法,注解中的Signature决定了你的代码对哪些方法拦截,update实际上针对**修改(Update)、删除(Delete)生效,query是对查询(Select)**生效。

下面给出针对Select注入查询条件限制的完整代码:


    private String processSelectSql(String sql, List<PermissionRule> rules, UserDefaultZimpl principal) {
        try {
            String replaceSql = null;
            Select select = (Select) CCJSqlParserUtil.parse(sql);
            PlainSelect selectBody = (PlainSelect) select.getSelectBody();
            String mainTable = null;
            if (selectBody.getFromItem() instanceof Table) {
                mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", "");
            } else if (selectBody.getFromItem() instanceof SubSelect) {
                replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, principal);
            }
            if (!ValidUtil.isEmpty(replaceSql)) {
                sql = sql.replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql);
            }
            String mainTableAlias = mainTable;
            try {
                mainTableAlias = selectBody.getFromItem().getAlias().getName();
            } catch (Exception e) {
                log.debug("当前sql中, " + mainTable + " 没有设置别名");
            }


            String condExpr = null;
            PermissionRule realRuls = null;
            for (PermissionRule rule :
                    rules) {
                for (Object roleStr :
                        principal.getRoles()) {
                    if (rule.getRoles().indexOf("," + roleStr + ",") != -1) {
                        if (rule.getFromEntity().indexOf("," + mainTable + ",") != -1) {
                            // 若主表匹配规则主体,则直接使用本规则
                            realRuls = rule;

                            condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", mainTable).replace("{me.a}", mainTableAlias);
                            if (selectBody.getWhere() == null) {
                                selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr));
                            } else {
                                AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                selectBody.setWhere(and);
                            }
                        }

                        try {
                            String joinTable = null;
                            String joinTableAlias = null;
                            for (Join j :
                                    selectBody.getJoins()) {
                                if (rule.getFromEntity().indexOf("," + ((Table) j.getRightItem()).getName() + ",") != -1) {
                                    // 当主表不能匹配时,匹配所有join,使用符合条件的第一个表的规则。
                                    realRuls = rule;
                                    joinTable = ((Table) j.getRightItem()).getName();
                                    joinTableAlias = j.getRightItem().getAlias().getName();

                                    condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", joinTable).replace("{me.a}", joinTableAlias);
                                    if (j.getOnExpression() == null) {
                                        j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr));
                                    } else {
                                        AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                        j.setOnExpression(and);
                                    }
                                }
                            }
                        } catch (Exception e) {
                            log.debug("当前sql没有join的部分!");
                        }
                    }
                }
            }
            if (realRuls == null) return sql; // 没有合适规则直接退出。

            if (sql.indexOf("limit ?,?") != -1 && select.toString().indexOf("LIMIT ? OFFSET ?") != -1) {
                sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?");
            } else {
                sql = select.toString();
            }

        } catch (JSQLParserException e) {
            log.error("change sql error .", e);
        }
        return sql;
    }

重点思路
重点其实就在于Sql的解析和条件注入,使用开源项目JSqlParser

  • 解析出MainTable和JoinTable。from之后跟着的称为MainTable,join之后跟着的称为JoinTable。这两个就是我们PermissionRule需要匹配的表名,PermissionRule::fromEntity字段。
  • 解析出MainTable的where和JoinTable的on后面的条件。使用and连接原本的条件和待注入的条件,PermissionRule::exps字段。
  • 使用当前登录的用户信息(放在缓存中),替换条件表达式中的值。
  • 某些情况需要忽略权限,可以考虑使用ThreadLocal(单机)/Redis(集群)来控制。

结束语

想要达到无感知的数据权限控制,只有机制控制这么一条路。本文选择的是通过底层拦截Sql语句,并且针对对应表注入条件语句这么一种做法。应该是非常经济的做法,只是基于文本处理,不会给系统带来太大的负担,而且能够达到理想中的效果。大家也可以提出其他的见解和思路。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java利用Mybatis进行数据权限控制 的相关文章

  • JavaEE 8 教程,在 hello1 项目上部署失败

    我正在尝试学习 Java EE 8 我遵循了官方指南https javaee github io tutorial https javaee github io tutorial 但我有这个问题 cargo maven2 plugin 1
  • Java Swing:清除JList而不触发监听器

    我的情况如下 我有一个 JList 只要在列表中进行选择 它就会触发搜索 使用 ListSelectionListener 我正在尝试使用以下命令重置列表上的选择list clearSelection 这样做的问题是使用clearSelec
  • .java 和 .scala 类之间是否可能存在循环依赖?

    假设我在 java 文件中定义了类 A 在 scala 文件中定义了类 B A 类使用 B 类 B 类使用 A 类 如果我使用 java 编译器 则会出现编译错误 因为 B 类尚未编译 如果我使用scala编译器A类将找不到 有没有可以同时
  • 使用多个构造函数创建不可变类

    我正在阅读这一页 https docs oracle com javase tutorial essential concurrency imstrat html关于在 Java 中创建不可变类 并决定根据页面上概述的规范修改我正在编写的类
  • JUnit 使用 Mockito 测试异步方法

    我已经使用 Spring Framework 版本 5 0 5 RELEASE 在 Java 1 8 类中实现了异步方法 public class ClassToBeTested Autowired private MyComponent
  • 如何在Spring的applicationContext.xml中指定默认范围来请求范围?

    我想让所有 bean 请求默认作用域 但是 Spring 文档说默认作用域是 Singleton 第 3 4 1 和 3 4 2 节http static springsource org spring docs 2 5 x referen
  • 方法不必要地被调用?

    我有一个 BaseActivity 它可以通过其他所有活动进行扩展 问题是 每当用户离开 暂停 活动时 我都会将音乐静音 我也不再接听电话 问题是 onPause每当用户在活动之间切换时就会被调用 这意味着应用程序不必要地静音和停止tele
  • 我们可以在三元运算符(Java)中使用命令吗?

    这是一个工作代码 String a first String b second String object System out println object null a b 但它不是 String a first String b se
  • 会话 bean 中的 EntityManager 异常处理

    我有一个托管无状态会话 bean 其中注入了 EntityManager em 我想做的是拥有一个具有唯一列的数据库表 然后我运行一些尝试插入实体的算法 但是 如果实体存在 它将更新它或跳过它 我想要这样的东西 try em persist
  • grails 上的同步块在 Windows 上有效,但在 Linux 上无效

    我有一个 grails 应用程序 它依赖于服务中的同步块 当我在 Windows 上运行它时 同步按预期工作 但当我在 ams linux 上运行时 会出现 StaleObjectStateException 该问题在以下示例中重现 cla
  • Java G1 GC 处理引用对象运行缓慢

    我已经在 J ava 上运行了计数器 它24小时工作 每秒点击通过100次左右 白天 GC 处理时间从 20 60 毫秒缓慢上升到 10000 60000 毫秒 然后下降到 20 60 毫秒 这种模式不时地重复 从 GC 日志中我发现 GC
  • Java String.format 向整数添加空格

    我有一小段代码 我不明白输出 此输出向我的字符串格式文本添加空格 我做错了什么吗 public class HelloWorld public static void main String args int a1 540 int a2 4
  • java 属性文件作为枚举

    是否可以将属性文件转换为枚举 我有一个包含很多设置的属性文件 例如 equipment height equipment widht equipment depth and many more like this and not all a
  • 哪种 Java DOM 包装器是最好或最受欢迎的? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • @TestPropertySource 不适用于 Spring 1.2.6 中使用 AnnotationConfigContextLoader 的 JUnit 测试

    似乎我在 Spring 4 1 17 中使用 Spring Boot 1 2 6 RELEASE 所做的任何事情都不起作用 我只想访问应用程序属性并在必要时通过测试覆盖它们 无需使用 hack 手动注入 PropertySource 这不行
  • 用 Java 创建迷宫求解算法

    我被分配了用 Java 创建迷宫求解器的任务 这是任务 Write an application that finds a path through a maze The maze should be read from a file A
  • 将字符串中的字符向左移动

    我是 Stack Overflow 的新手 有一道编程课的实验室问题一直困扰着我 该问题要求我们将字符串 s 的元素向左移动 k 次 例如 如果输入是 Hello World 和3 它将输出 lo WorldHel 对于非常大的 k 值 它
  • 防止 Firebase 中的待处理写入事务不起作用

    我的目标是在单击按钮时将名称插入 Cloud Firestore 中 但如果用户未连接到互联网 我不希望保存处于挂起状态 我不喜欢 Firebase 保存待处理写入的行为 即使互联网连接已恢复 我研究发现Firebase 开发人员建议使用事
  • Android同步onSensorChanged?

    这是我的问题的后续 Android线程可运行性能 https stackoverflow com questions 36395440 android thread runnable performance 我在理解应用程序的同步方法时遇到
  • 监控 Java 应用程序上的锁争用

    我正在尝试创建一个小基准 在 Groovy 中 以显示几个同步方法上的高线程争用 当监控自愿上下文切换时 应该会出现高争用 在 Linux 中 这可以通过 pidstat 来实现 程序如下 class Res private int n s

随机推荐

  • C中字符串操作

    字符串可以看作一个数组 它的每个元素是字符型的 例如字符串 Hello world n 图示如下 H e l l o w o r l d n 0 15个字符 注意每个字符串末尾都有一个字符 0 做结束符 这里的 0是ASCII码的八进制表示
  • 初识运营,明晰运营的学习路径

    关于运营的思考 问题1 运营是什么 运营到底是做什么工作的 如题 到底什么是运营 为什么我们所接触到的很多运营都不太一样 有的运营就是每天追寻互联网热点 加班加点的写文案 有的运营每天就是在不同的群里和成千上万的人唠嗑 有的运营活跃在不同的
  • html获取text输入框中的值

    1 在head中引用jquery 2 定义一个text输入框
  • 指针用作函数参数、指针型函数和函数指针

    指针用作函数参数 以前我们学过的函数参数要么是基本数据类型的变量 要么是类的对象 又或者是数组名 前几讲学到的指针同样可以用作函数参数 指针作函数形参时 我们调用此函数将实参值传递给形参后 实参和形参指针变量将指向相同的内存地址 那么在被调
  • Linux主要命令功能

    1 dmesg 主要用来显示内核信息 使用dmesg可以有效诊断机器硬件故障或者添加硬件出现的问题 另外使用dmesg可以确定你的服务器安装了那些硬件 每次系统重启 系统都会检查所有硬件并将信息记录下来 执行 bin dmesg命令可以查看
  • 第十届蓝桥杯真题-灵能传输

    题目 OJ https www lanqiao cn problems 196 learning 考点 前缀和 贪心 思路 题目意思就是希望通过灵能交换后使得不稳定度最小 假设对a i 进行灵能传输 可以发现前缀和s i 1 和s i 进行
  • Java值传递和引用传递详细说明(详细分析)

    1 形参与实参 我们先来重温一组语法 形参 方法被调用时需要传递进来的参数 如 func int a 中的a 它只有在func被调用期间a才有意义 也就是会被分配内存空间 在方法func执行完成后 a就会被销毁释放空间 也就是不存在了 实参
  • FormData同时传输多个文件和其他数据

    近日有个需求是 在web的对话框中 用户可以输入文本内容和上传附件 附件的数量不限 所有附件总和大小不超过20M 这个实现的方法不止一种 比如之前的后端同事是要求 文件和文本分开传输 文件用一个单独接口上次 上传成功后返回一个id 把这个i
  • 【论文翻译】Iterative Geometry Encoding Volume for Stereo Matching and Multi-View Stereo(CVPR 2023)

    一 论文简述 1 第一作者 Gangwei Xu 2 发表年份 2023 3 发表期刊 CVPR 4 关键词 立体匹配 MVS 几何编码体 GRU 3D卷积 5 探索动机 RAFT的全对相关体缺乏非局部几何知识 难以处理病态区域的局部模糊
  • perp系列之五:perp安装

    perp系列之五 perp安装 版本说明 版本 作者 日期 备注 0 1 ZY 2019 5 29 初稿 目录 文章目录 perp系列之五 perp安装 版本说明 目录 概要 描述 激活 升级 例子 概要 vi conf mk make m
  • Vmware虚拟机设置静态IP地址

    一 安装好虚拟后在菜单栏选择编辑 虚拟网络编辑器 打开虚拟网络编辑器对话框 选择Vmnet8 Net网络连接方式 随意设置子网IP 点击NAT设置页面 查看子网掩码和网关 后面修改静态IP会用到 二 检查宿主机VM8 网卡设置 打开网络和共
  • 欧科云链OKLink:2023年6月安全事件盘点

    一 基本信息 2023年6月REKT和RugPull事件约造成约1000万美元损失 相比上月有显著下降 但安全事件发生数量依旧不减 其中被废弃的Atlantis Loans遭受治理攻击 造成的损失超250万美元 对该协议有过授权的用户需及时
  • Faster R-CNN详解

    Faster R CNN代码实现详解参见 faster rcnn源码详解 樱花的浪漫的博客 CSDN博客 faster rcnn源码 Faster rcnn详细注释版源码地址 faster rcnn源码详细注释版 每一步均有详细批注 深度学
  • 目标检测 实践

    文章目录 0 数据标注via 一 添加图片 二 定义标记类型 Attributes 三 标注 四 导出标注文件 1 模型 1 1 数据准备 1 2 模型训练 1 3 模型使用 1 4 改进方向 0 数据标注via via工具的界面如下图所示
  • python3中多项式创建_利用 Python3 ,实现数学科学计算

    SymPy是符号数学的Python库 旨在用一套强大的符号计算体系完成诸如多项式求值 求极限 解方程 求积分 微分方程 级数展开 矩阵运算等等计算问题 虽然Matlab的类似科学计算能力也很强大 但是Python以其语法简单 易上手 异常丰
  • JDBC和数据库连接池

    JDBC 基本介绍 JDBC原理 模拟JDBC public class MysqlJdbcImp implements JdbcInterface Override public Object getConnection System o
  • 解决element ui select下拉框不回显数据问题

    最近在做一个项目 项目的后端是地址 https github com wangyuanjun008 wyj springboot security git 前端地址是 https github com wangyuanjun008 wyj
  • 用通俗易懂的方式讲解:lightGBM 算法及案例(Python 代码)

    文章目录 1 介绍lightGBM之前 1 1 lightGBM演进过程 1 2 AdaBoost算法 1 3 GBDT算法以及优缺点 1 4 启发 2 什么是 lightGBM 3 lightGBM 原理 3 1 基于Histogram
  • 开源中文大语言模型集合【2023-06-19】

    整理开源的中文大语言模型 以规模较小 可私有化部署 训练成本较低的模型为主 包括底座模型 垂直领域微调及应用 数据集与教程等 自ChatGPT为代表的大语言模型 Large Language Model LLM 出现以后 由于其惊人的类通用
  • Java利用Mybatis进行数据权限控制

    权限控制主要分为两块 认证 Authentication 与授权 Authorization 认证之后确认了身份正确 业务系统就会进行授权 现在业界比较流行的模型就是RBAC Role Based Access Control RBAC包含