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

  • 背景描述

    我们目前的后端代码架构,看样子有点像是贫血模型DDD的风格,简单来说,就是

  • 在Service业务层的方法中,多属性的模型里,入参命名为xxxDTO,返回命名为xxxVO;
  • 在Controller接口层就是用泛型多包装了一下,入参时ApiRequest,返回时ApiResponse ,
  • 在Repository持久层,我司采用的是增强mybatis的框架,Mybatis-Plus,所有的数据库返回模型类,都放在包名po下,类名一般不加PO后缀。
  • 有些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 的包装类,或许可以抽取出公共接口层,封装一下,进行代码优化。

  • 23
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
软件系统设计⽅案 软件系统设计⽅案 前⾔ 前⾔ 本⽂根据⾼级软件⼯程课上所学知识,对⼯程实践项⽬-⽹上书城进⾏软件系统分析和设计,最终形成软件系统概念原型。 参考资料: ⼀、系统架构 ⼀、系统架构 系统采⽤MVC架构,MVC包括模型层(Model)、视图层(View)、控制器层(Controller) Model代表⼀个存取数据的对象及其数据模型。 View代表模型包含的数据的表达⽅式,⼀般表达为可视化的界⾯接⼝。 Controller作⽤于模型和视图上,控制数据流向模型对象,并在数据变化时更新视图。控制器可以使视图与模型分离开解耦合。 其中控制器创建模型; 控制器创建⼀个或多个视图,并将它们与模型相关联; 控制器负责改变模型的状态; 当模型的状态发⽣改变时,模 型会通知与之相关的视图进⾏更新。 ⼆、项⽬视图 ⼆、项⽬视图 1、分解视图 、分解视图 分解是构建软件架构模型的关键步骤,分解视图也是描述软件架构模型的关键视图,⼀般分解视图呈现为较为明晰的分解结构特点。 2、依赖视图 、依赖视图 依赖视图展现了软件模块之间的依赖关系。⽐如⼀个软件模块A调⽤了另⼀个软件模块B,那么我们说软件模块A直接依赖软件模块B。如果 ⼀个软件模块依赖另⼀个软件模块产⽣的数据,那么这两个软件模块也具有⼀定的依赖关系。 3、执⾏视图 、执⾏视图 执⾏视图展⽰了系统运⾏时的时序结构特点,⽐如流程图、时序图等。执⾏视图中的每⼀个执⾏实体,⼀般称为组件(Component),都是 不同于其他组件的执⾏实体。如果有相同或相似的执⾏实体那么就把它们合并成⼀个。 执⾏实体可以最终分解到软件的基本元素和软件的基 本结构,因⽽与软件代码具有⽐较直接的映射关系。在设计与实现过程中,我们⼀般将执⾏视图转换为伪代码之后,再进⼀步转换为实现代 码 3.1 顾客 3.2 员⼯ 3.3 管理员 4、实现视图 、实现视图 实现视图是描述软件架构与源⽂件之间的映射关系。⽐如软件架构的静态结构以包图或设计类图的⽅式来描述,但是这些包和类都是在哪些 ⽬录的哪些源⽂件中具体实现的呢?⼀般我们通过⽬录和源⽂件的命名来对应软件架构中的包、类等静态结构单元,这样典型的实现视图就 可以由软件项⽬的源⽂件⽬录树来呈现。 实现视图有助于码农在海量源代码⽂件中找到具体的某个软件单元的实现。实现视图与软件架构的静态结构之间映射关系越是对应的⼀致性 ⾼,越有利于软件的维护,因此实现视图是⼀种⾮常关键的架构视图。 src       源代码⽬录 -main     存放实现类的源代码 --bean       model类 --controller        控制器类 --dao       持久层--service       实现业务功能服务 --util       ⼯具类 -test     测试类 三、系统运⾏环境 三、系统运⾏环境 操作系统:Windows 10 开发语⾔:Java 开发⼯具:IntelliJ IDEA 前端框架:Vue 后端框架:Spring + SpringMVC + Mybatis 数据库: MySQL 技术选型说明:Spring的IOC特性,将对象之间的依赖关系交给了Spring控制,⽅便解耦,简化了开发。其AOP特性,对重复模块进⾏集 中,实现事务,⽇志,权限的控制;SpringMVC是使⽤了MVC设计思想的轻量级web框架,对web层进⾏解耦,与Spring⽆缝衔接,有着灵 活的数据验证,格式化,数据绑定机制;Mybatis中数据库的操作(sql)采⽤xml⽂件配置,解除了sql和代码的耦合提供映射标签,⽀持对象和 和数据库orm字段关系的映射,⽀持对象关系映射标签,⽀持对象关系的组建。 四、数据库设计 四、数据库设计 本项⽬⽬前共有4张数据库表 1.顾客表 字段名 类型 长度 是否可为空 customer_id int 10 N name varchar 20 N statement varchar 50 Y password varchar 20 N 2.普通员⼯表 字段名 类型 长度 是否可为空 worker_id int 10 N name varchar 20 N statement varchar 50 Y password varchar 20 N 3.书籍表 字段名 类型 长度 是否可为空 book_id int 10 N name varchar 20 N statement varchar 50 Y price int 10 N 4.订单表 字段名 类型 长度 是否可为空 order_id int 10 N name varchar 20 N statement varchar 50 Y belong Customer 20 Y 五、系统概念原
### 回答1: Java后端系统设计文档是一个详细描述Java后端系统的架构、模块、功能和设计思路的文档。 该文档通常由以下几个部分组成: 1. 引言:介绍Java后端系统的背景和目标,提供系统设计的整体概述。 2. 系统架构:描述系统的整体架构和组成部分,包括系统的分层设计、模块划分、数据流和控制流的图表等。同时也介绍了系统所运用的技术栈和工具。 3. 功能模块:详细描述系统的各个功能模块,包括每个模块的功能描述、输入输出、接口设计和依赖关系。可以使用UML类图或流程图展示各个模块的关系和交互。 4. 数据库设计:介绍系统所使用的数据库,包括数据库表的设计、表之间的关系以及查询语句的优化等。可以使用ER图来表示数据库的结构。 5. 接口设计:描述系统与其他外部系统或者前端系统的接口设计,包括输入输出参数的定义、接口调用方式和数据格式。 6. 性能优化:介绍系统性能的优化策略,包括缓存设计、负载均衡、并发控制和数据库优化等。 7. 安全设计:描述系统的安全设计,包括用户身份验证、权限控制和数据加密等,保证系统的安全性和可靠性。 8. 部署和运维:介绍系统部署和运维的步骤和方案,包括服务器配置、备份策略和监控系统等。 Java后端系统设计文档是开发人员和项目经理之间的重要沟通工具,能够帮助团队了解项目需求,清晰明了地规划和调整系统设计。同时,它也是项目开发过程中的参考文档,使开发人员能够按照规范进行设计和开发,并且方便后期维护和升级。 ### 回答2: Java后端系统设计文档是指在开发Java后端系统时,为了指导开发人员进行系统设计和实现而编写的技术文档。该文档通常包含以下几个方面的内容: 1.系统概述:对系统整体进行介绍,包括系统的目标、功能、运行环境等。 2.系统架构:描述系统的整体架构设计,包括系统的层次结构、模块划分、模块之间的关系等。同时,也需要说明所采用的技术和框架,如Spring、Hibernate等。 3.数据库设计:对系统所需的数据库进行设计,包括数据库表结构、表之间的关系、索引设计等。同时,也需要说明数据库的选择和优化策略。 4.接口设计:描述系统与外部系统或者用户之间的接口设计,包括接口的规范、参数、返回值等。例如,系统对外部提供的API接口,或者与前端页面交互的接口。 5.模块设计:对系统各个模块的详细设计进行说明,包括模块的功能、类的设计、方法的设计等。同时,也需要详细说明模块之间的调用关系和数据流动。 6.安全性设计:对系统的安全性进行设计,包括用户认证、权限控制、数据加密等。同时,也需要说明所采用的安全策略和技术。 7.性能设计:对系统的性能进行设计,包括系统的吞吐量、响应时间等。同时,也需要说明所采用的性能优化策略和工具。 8.部署和维护:描述系统的部署过程和维护方法,包括系统的部署环境配置、备份恢复策略、系统更新等。 通过编写Java后端系统设计文档,可以为开发人员提供清晰的开发指导,有利于快速高效地开发系统,并且可以有效提升系统的可维护性和可扩展性。 ### 回答3: Java后端系统设计文档是指在开发Java后端系统时所编写的详细流程和实现方法的文档。 系统设计文档主要包含以下内容: 1. 系统概述:对系统的整体概况进行描述,包括系统的目标、功能和特性等。 2. 系统架构设计:描述系统的整体架构,包括前端、后端数据库、缓存、消息队列等的组件及其关系。 3. 数据库设计:定义系统中所需存储的数据结构、表的关系和字段定义,并进行性能和扩展性的考虑。 4. RESTful API 设计:描述系统与外部服务或其他系统的接口设计,包括接口的输入输出、接口参数和返回结果的定义。 5. 高可用性与容灾设计:描述如何保证系统的高可用性和容灾能力,包括负载均衡、故障恢复、数据备份和恢复等措施。 6. 安全设计:描述系统的安全策略和措施,包括用户认证、权限控制、数据加密等。 7. 性能优化设计:描述系统的性能优化方案,包括系统的横向和纵向扩展、缓存、异步处理等。 8. 日志与监控设计:描述系统的日志记录与监控方案,包括日志的级别、格式和存储方式,以及系统性能指标的监控和报警。 系统设计文档是开发过程中的重要工作产品,它对整个系统的开发和维护具有指导性作用。通过编写系统设计文档可以帮助开发人员明确系统需求、提前预判问题并提供解决方案,促进团队协作、减少沟通成本,同时也方便日后系统的维护和升级。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值