数据权限方案设计(后端)

news2024/11/15 21:33:24
  • 有些BO模型放在bo包下用来装一些SQL语句查询返回,用于在Service层中进行处理业务逻辑,不会出现在Controller接口层的包装中。
  • 在持久层的基础组件中,我司业务种用的是Mysql5.7版本。
  • 在前后端开发使用的是分离技术,即前端使用流行的VUE+Element-UI ,后端提供json数据格式的http接口。

        在贫血模型的大部分情况下,各层的各种O基本都是只有属性,也可以叫字段 的get,set;而且很多时候,各层的O直接的属性和字段差异都不大,所以为了解决各种O之间的数据传递问题,我发现core项目代码中大部分是一半是用hutool提供的BeanUtil中的copyProperties方法,另一大半的是用的是bop脚手架封装的BeanUtils的convert或者copyProperties;至于 common-lang3提供的或者Spring提供的 BeanUtils几乎没有!

设计思路

        在数据权限模块下,我们坚持的原则是数据库驱动原理去做的,也就是数据库有的字段,我们才能做处理。我们基本只关心分页,列表,详情,修改这4种接口,(分页,列表,详情)对应select语句,(修改)对应update语句。用图来描述如下一次HTTP请求中的流程:

        如上图,对于【行权限】的处理原理,就是将配置的条件,转化成SQL语句种where/join后面的条件,拦截SQL语句并改写,从而达到行权限过滤数据行的目的!在针对【列权限】的处理,最开始的想法是将值进行修改,以达到传递权限标识的目的,后来在讨论过程中一致认为改值的风险太大,不管是针对前端还是后端风险都大,所以后来决定给前端的json结构中原字段同级返回多一个后缀字段传递标识。由于原值也一并返回了,所以【列权限】的update语句就可以暂时不用处理。

        在一次HTTP请求中,在java应用里面,从接收参数直到给前端返回json,所有的代码都是在一个线程下执行的。初步设想就是用Http拦截器在执行SQL之前,根据用户ID,从redis中拿到用户的【数据权限配置】放到ThreadLocal里,再使用Mybatis的拦截器从ThreadLocal里读取到配置进行修改SQL的处理,下面的图给大家详细介绍下,数据权限的整体逻辑和方案:

        在上图中,围绕redis的操作逻辑比较简单,日常工作用过redis基本可以理解。难点在于围绕【Mybatis框架及其拦截器】这个地方去做处理,最开始领导建议用anltr4去解析SQL,并改写SQL。后来发现在我们项目依赖的mybatis-plus里,依赖了jsqlparser,所以后面重点是去研究jsqlparser解析并修改SQL的功能,以达到处理【行权限】的目的。(PS:其实仔细一想,我们这个是Saas系统,而且用的是数据库行隔离方案,也就是绝大部分业务表都有tenent_id这个字段,Mybatis-Plus已经提供这个租户隔离的功能了,而且免费,学习研究一下相关源码,实现行权限也不算是太难。)

        最开始在考虑【列权限】实现方案时,初步设想是要在处理update语句的时候进行修改SQL语句,(举个栗子,比如name不可编辑,那update table set name = ‘abc’,update_time = ‘xxx’ where id = 123这句语句中将set 里面 的 name 字段去掉;)也是要在【Mybatis框架及其拦截器】这个地方去做处理,不过后来由于采用的列权限方案是把原值也返回给前端,这样这里就不用去处理update语句了。

        这里简单聊下Mybatis,它是一个轻量级的ORM框架;在ORM框架问世之前,大家基本是基于jdbc去操作数据库的,手动的将DTO里的字段作为参数set到PreparedStatement里,在返回的ResultSet里迭代,一个个get字段再set到VO模型里。有了ORM后就省去很多这种无脑操作,专注于业务逻辑了。详细的Mybatis层次结构,网上一搜一大把,这里不贴图了。在我们这里,只需要关心拦截器怎么用,所以下面简单通过一个图了解下Mybatis拦截器的相关知识:

        Mybatis支持我们去拦截Executor,StatementHandler,ParameterHandler,ResultSetHandler这4个地方的方法,研究发现sql语句是包装在BoundSql这个对象中,所以针对select语句,处理【行权限】,增加where/join条件,统一在StatementHandler的prepare方法中拦截SQL并进行修改。至于【列权限】,上文提到过,我们不需要修改update语句了,所以只需要专心处理怎样把select语句中涉及的字段,通过和ThreadLocal中传递过来的字段(表名和列名以及是否隐藏和是否可编辑)做比较,然后在json序列化的时候进行处理。为了传递列权限标识到json那层,我们决定将字段进行包装一层返回,如下以常见的String类型字段举例:

@Getter
@JsonSerialize(using = StringPrivilegeSerializer.class)
public class StringPrivilege implements java.io.Serializable {

    public static final StringPrivilege EMPTY = new StringPrivilege();
    String value;
    String privilege;
    public StringPrivilege(String value) {
        this.value = value;
    }
    public void setStringPrivilege(String value, String privilege) {
        this.value = value;
        this.privilege = privilege;
    }

    //忽略其他的 equals hashCode toString 方法,那3个方法基本上重写时候只用value属性去计算即可
}

JSON序列化器StringPrivilegeSerializer的代码如下

@Component
public class StringPrivilegeSerializer extends JsonSerializer<StringPrivilege> {
    @Override
    public void serialize(StringPrivilege obj, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //每个包装类引入单例 EMPTY对象
        if(StringPrivilege.EMPTY.equals(obj)){
            gen.writeNull();
        }else {
            gen.writeString(obj.getValue());
        }

        if (obj.getPrivilege() != null) {
            gen.writeStringField(gen.getOutputContext().getCurrentName() + "__privilege", obj.getPrivilege());
        }
    }
}

        上面提到的,如何通过 从 select的列 到json多返回一个同级字段,StringPrivilege 包装类起到了传递权限标识作用,但是往包装类里的privilege设置值的关键逻辑就在Mybatis提供的TypeHandler 里,代码如下:

@Component
@Slf4j
public class StringPrivilegeTypeHandler extends BaseTypeHandler<StringPrivilege> {

    //...忽略其他的方法

    @Override
    public StringPrivilege getNullableResult(ResultSet rs, String columnName) throws SQLException {
        List<List<String>> fieldList = ThreadLocalContent.getAllTableColumnName();
        Set<ColumnRight> columnRights = ThreadLocalContent.getColumnRights();
         // 拿到ThreadLocal里的列权限配置,和 columnName 进行过滤匹配,算出是 隐藏 --- 还是 只读 -r-
        String right = calRight(columnName,fieldList,columnRights);
        String bd = rs.getString(columnName);
        return new StringPrivilege(bd, right);
    }

}

        上述只是用了常见的String字段举例,真实代码里需要把 Long , Integer , BigDecimal 也包装一遍,至于Double等其他数据类型,尽量转成使用前面的4种,否则不支持数据权限控制。

        然后在PO/VO/BO中可以这么用

@Data
public class DemoPO implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主键")
    @TableId("id")
    private Long id;

    @ApiModelProperty(value = "昵称")
    @TableField("nick_name")
    private StringPrivilege nickName;   

}

        为了解决PO,VO,BO,DTO之间的转换问题,我们又提供了一个增强的属性拷贝工具

@Slf4j
public class EnhancedBeanUtil {

    public static void copyProperties(Object source, Object target){
        //其他代码
        if (value instanceof String && targetPd.getPropertyType() == StringPrivilege.class) {
            StringPrivilege sp = new StringPrivilege();
            sp.setValue((String) value);
            Method writeMethod = targetPd.getWriteMethod();
            if (!writeMethod.isAccessible()) {
                writeMethod.setAccessible(true);
            }
            writeMethod.invoke(target, sp);
        }
        //其他代码
        //如果原值是null,上面不会进入任何一个 ,但如果目标是包装类,则设置为EMPTY
        if (value == null && targetPd.getPropertyType() == StringPrivilege.class){
            value = StringPrivilege.EMPTY;
        }
        //为了防止null拷贝到包装类型上时候,从target直接get字段再getValue 产生NPE,在每个包装类引入单例 EMPTY对象
    }

}

        经过上文的全部分析过程,我们针对数据权限的【行】和【列】都有了统一的处理方案了!

未做事项

        对于String,Long , Integer , BigDecimal 的包装类,或许可以抽取出公共接口层,封装一下,进行代码优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1411564.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RISC-V常用汇编指令

RISC-V寄存器表&#xff1a; RISC-V和常用的x86汇编语言存在许多的不同之处&#xff0c;下面将列出其中部分指令作用&#xff1a; 指令语法描述addiaddi rd,rs1,imm将寄存器rs1的值与立即数imm相加并存入寄存器rdldld t0, 0(t1)将t1的值加上0,将这个值作为地址&#xff0c;取…

6家券商综合评级上升,12月券商App终端业务体验评测报告发布

随着移动金融服务的盛行&#xff0c;手机 App 炒股成为广大股民普遍的选择。股市行情变幻莫测&#xff0c;行情推送速度会影响到投资者的交易决策&#xff0c;委托下单与撤单等关键操作环节的响应性能又会极大影响投资者的收益。由此&#xff0c;行情数据的推送实时性和交易的快…

小程序系列--14.小程序分包

一、基础概念 1. 什么是分包 分包指的是把一个完整的小程序项目&#xff0c;按照需求划分为不同的子包&#xff0c;在构建时打包成不同的分包&#xff0c;用户在使用时按需进行加载。 2. 分包的好处 3. 分包前项目的构成 4. 分包后项目的构成 5. 分包的加载规则 6. 分包的…

vue3预览pdf文件的几种方法

vue3预览pdf集中方法 方法一&#xff1a; iframe&#xff1a;这种方法显示有点丑 <iframesrc"E:\\1.pdf"frameborder"0"style"width: 80%; height: 100vh; margin: auto; display: block"></iframe>方法二&#xff1a; 展示效果&…

Objective-C方法的声明实现及调用

1.无参数的方法 1)声明 a.位置&#xff1a;在interface括弧的外面 b.语法&#xff1a; - (返回值类型)方法名称; interface Person : NSObject -(void) run; end 2)实现 a.位置&#xff1a;在implementation中实现 b.语法&#xff1a;加大括弧将方法实现的代码写在大括孤之中 …

浅出深入-机器学习

文章目录 一、K近邻算法1.1 先画一个散列图1.2 使用K最近算法建模拟合数据1.3 进行预测1.4 K最近邻算法处理多元分类问题1.5 K最近邻算法用于回归分析1.6 K最近邻算法项目实战-酒的分类1.6.1 对数据进行分析1.6.2 生成训练数据集和测试数据集1.6.3 使用K最近邻算法对数据进行建…

抽象工厂模式-C#实现

该实例基于WPF实现&#xff0c;直接上代码&#xff0c;下面为三层架构的代码。 一 Model using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace 设计模式练习.Model.抽象工厂模式 {public abstrac…

学习笔记之 机器学习之预测雾霾

文章目录 Encoder-DecoderSeq2Seq (序列到序列&#xff09; Encoder-Decoder 基础的Encoder-Decoder是存在很多弊端的&#xff0c;最大的问题就是信息丢失。Encoder将输入编码为固定大小的向量的过程实际上是一个“信息有损的压缩过程”&#xff0c;如果信息量越大&#xff0c;…

Rollup:打包 TypeScript - React 组件库

调用浏览器摄像头拍照组件 1、前提1、安装依赖2、添加 rollup.config.js 配置3、修改 package.json3.1 添加打包命令3.2 添加组件入口3.3 添加组件声明入口3.4 浏览器支持 1、前提 1.1 通过 create-react-app take-photo --template 创建前端应用 1.2 添加组件 TakePhoto (拍照…

华为三层交换机之基本操作

Telnet简介 Telnet是一个应用层协议,可以在Internet上或局域网上使用。它提供了基于文本的远程终端接口&#xff0c;允许用户在本地计算机上登录到远程计算机&#xff0c;然后像在本地计算机上一样使用远程计算机的资源。Telnet客户端和服务器之间的通信是通过Telnet协议进行的…

<蓝桥杯软件赛>零基础备赛20周--第17周--并查集

报名明年4月蓝桥杯软件赛的同学们&#xff0c;如果你是大一零基础&#xff0c;目前懵懂中&#xff0c;不知该怎么办&#xff0c;可以看看本博客系列&#xff1a;备赛20周合集 20周的完整安排请点击&#xff1a;20周计划 每周发1个博客&#xff0c;共20周。 在QQ群上交流答疑&am…

JavaSE基础学习

一、编程入门 二、Java语言概述 三、Java基本语法 四、程序流程控制 五、数组 六、面向对象(上) 数组工具类的封装: 七、面向对象(中) 八、面向对象(下) 九、异常处理 十、多线程 十一、常用类 十二、枚举类与注解 十三、集合 十四、泛型 十五、IO流 十六、网络编程 十七、反射…

【论文阅读】Grasp-Anything: Large-scale Grasp Dataset from Foundation Models

文章目录 Grasp-Anything: Large-scale Grasp Dataset from Foundation Models针对痛点和贡献摘要和结论引言相关工作Grasp-Anything 数据集实验 - 零镜头抓取检测实验 - 机器人评估总结 Grasp-Anything: Large-scale Grasp Dataset from Foundation Models Project page&…

【重点】【DP】123.买卖股票的最佳时机III

题目 法1&#xff1a;单次遍历&#xff0c;Best! class Solution {public int maxProfit(int[] prices) {int f1 -prices[0], f2 0, f3 -prices[0], f4 0;for (int i 1; i < prices.length; i) {f1 Math.max(f1, -prices[i]);f2 Math.max(f2, f1 prices[i]);f3 Ma…

Cesium中实现流体模拟

流体模拟 流体模拟是指通过数学模型和计算机算法来模拟流体行为的过程。它可以用来研究和预测各种液体和气体的运动、相互作用和变形。 流体模拟有多种方法&#xff0c;下面列举了几种常见的方法&#xff1a; 网格方法&#xff1a;网格方法是最常用的流体模拟方法之一。它将模…

VSCode Vue项目中报错 [vue/require-v-for-key]

报错 [vue/require-v-for-key] Elements in iteration expect to have v-bind:key directives.eslint-plugin-vue 解决办法&#xff1a; 在设置里把这个取消勾选

Java 数据库连接

1&#xff0c;JDBC概述 在开发中我们使用的是java语言&#xff0c;那么势必要通过java语言操作数据库中的数据。这就是接下来要学习的JDBC。 1.1 JDBC概念 JDBC 就是使用Java语言操作关系型数据库的一套API 全称&#xff1a;( Java DataBase Connectivity ) Java 数据库连接 …

单片机14-17

目录 LCD1602 LCD1602液晶显示屏 直流电机驱动&#xff08;PWM&#xff09; LED呼吸灯 直流电机调速 AD/DA&#xff08;SPI通信&#xff09; AD模数转换 DA数模转换 红外遥控&#xff08;外部中断&#xff09; 红外遥控 红外遥控电机调速 LCD1602 LCD1602液晶显示屏 …

智能语音识别源码系统+语义理解+对话管理+语音合成 带完整的搭建教程

人工智能技术的不断发展&#xff0c;智能语音识别技术逐渐成为人们日常生活和工作中不可或缺的一部分。然而&#xff0c;目前市场上的智能语音识别产品大多存在一定的局限性&#xff0c;如识别率不高、功能单一等。为了解决这些问题&#xff0c;罗峰给大家分享一款基于智能语音…

pytorch学习笔记(十一)

优化器学习 把搭建好的模型拿来训练&#xff0c;得到最优的参数。 import torch.optim import torchvision from torch import nn from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear from torch.utils.data import DataLoaderdataset torchvision.datas…