从绫开始的后台管理系统(二)

那这一章就正式开始对ruoyi的后端配置进行分析。

ruoyi的后端模块主要包括权限系统,动态数据源、数据权限隔离、代码生成器、定时任务,common工具,还有后端接口,下面我们逐一进行分析。

我们也会讲一些关于工程结构,依赖管理配置方面的设计。

工程结构

工程结构主要分为两部分:pom结构和包结构。

pom结构

pom结构遵循oo(面向对象)原则,即配置尽量放在父工程中进行管理,同时子工程也要能够根据自己的需要进行调整(封装与继承),但注意在划分工程时不要成环,即子工程互相依赖。

image-20210217215151835

我们可以看到,属性配置properties,和依赖管理dependencyManagement,都是放在父工程ruoyi中,子工程只需要继承父工程就可以使用父工程的配置和依赖。

注意:注意打包结构packaging,自己选择是要打成jar,war,还是pom包。一般来说父工程是pom包,子项目打包时现在一般时jar包(内置tomcat),如果使用外部tomcat则要打成war包。

image-20210217220344888

包结构

包结构目前由两种模型驱动:mvc模型和DDD模型。mvc模型是传统的驱动模型,讲究的是视图与数据分离,由controller负责管理;DDD模型(领域驱动模型)是近几年的一种驱动模型,他讲究的是根据业务领域划分功能边界。

  • mvc模型

    mvc模型在模板渲染时代(jsp和freemarker)比较流行,自从前后端分离流行起来后,渐渐的大家觉得味不太对,因为没有视图需要渲染了,controller的责任好像不像以前那么沉重了,但是又找不到新的,合适的称呼;而且因为ssm架构,导致配置xml和配置类越来越多,不是很好管理,因此mvc才渐渐没落下去。

    mvc模型的基本结构为controller-dao-service,业务相关的类都依据这三者进行划分,有一些配置和工具类,则单独放在config和utils包中进行管理。

  • DDD模型

    DDD模型(领域驱动模型)因上述的发展原因被挖了出来,实际上互联网各种理论很早就有人提出来了,只是顺应时代发展才出现这种风水轮流转的局面。mvc模型不足以描述复杂的结构,因此才掏出了DDD。

    DDD模型讲究以业务边界划分领域,在业务边界有重合的地方,则单独补充一个业务融合领域。

    由于DDD讲究一个理解(业务理解和架构理解),因此并没有mvc模型划分的那么清晰。每个人都有自己的一套理解和最佳实践。简单的说是分为api-app-domain-infra四层,具体可以去看我的这篇文章DDD项目结构(领域驱动设计)

这里我不讲的太深,具体理论有各路大神比我讲的更好。

ruoyi的包结构有一些地方让我有点难受,它把service放在了依赖ruoyi-system中,而controller放到了ruoyi-admin中这样就导致我要改动代码有时就要改两遍。

我理解作者的意图,他是想把字典,系统信息等等服务作为一个依赖提供,以供其他包调用,将controller和service解耦。但是我还是比较喜欢放在同一个包里,统一对外提供服务,这个就属于仁者见仁智者见智了。

在我看来,一个模块是一个整体,controller是面向http提供服务,service和mapper是面向class提供服务,所以他们这种提供服务的属性没必要拆开,应该由调用方面对不同的场景是,选择不同的服务方法。

遇到这种情况我们会放在同一个包里。

权限系统

权限验证设计

ruoyi的权限系统是使用spring security的方案,token颁发是使用jwt+redis。即将token下发给客户端,且在服务器也存储一份。鉴权时在接口调用处进行鉴权。

token颁发

这个设计在我来看着实是有些臃肿。用户信息是用单独接口去获取的,那为什么还要用jwt去封装一次token,这样一来解析了两次(jwt一次,cache一次)才能获取到用户信息,真的有这个必要吗?

现在主流的方案有两种,jwttoken+redis

  1. jwt是无状态分布式的设计,类似于证书签署下发。
    • 优点
      1. jwt可以减少服务器压力。因为是无状态的设计,所以用户信息都存储在本体中被下发到客户端,客户端携带jwt访问资源时,可以直接读取到用户信息。特别是在分布式环境下,各个服务不需要再根据token去获取用户信息,鉴权中心也不需要再去存储用户信息。
    • 缺点
      1. jwt无法主动过期。因为是无状态的设计,jwt自身会包含一个过期时间,但是服务端无法让它主动过期或续签。比如我们想要修改用户的数据(如时区,权限等)这样就要使原有的jwt失效,重新颁发。要想主动过期必须要维护一个集合,以此来验证jwt是否有效。实际上证书注销也是这么做的,在https请求时会验证证书是否有效。
      2. jwt存储过多数据会造成http头负担过大。如果用户信息中有太多的属性,比如时区,登录地区,操作系统等,或者直接在里面维护一个权限list。这样会导致http头负担暴增,http开销变得极大。
      3. jwt未加密。jwt是使用base64编码,因此等同于明文。且使用base64编码增大数据体积,使2的缺点进一步被放大。
      4. 接口防刷及多端验证。持有jwt的客户端可以无穷端登录或者无限刷你的接口,这个时候你就要去管理jwt以防止这种问题。
  2. token+redis是类session中心化存储的设计。
    • 优点
      1. 可进行会话管理。因为用户信息和登录状态都是存储在服务器的,因此服务器可以控制会话,动态修改用户信息。并且这也是类session设计的优势所在,现在不推荐使用session的原因是因为session的跨域问题,安全问题以及存储扩展问题。
    • 缺点
      1. 信息存储会增加服务器负担,且需要选择合适的存储组件。因为是中心化管理,所以用户信息必须要放在服务器存储,且这类数据读写频率会非常高,不能用数据库进行管理,需要引入redis等其他高速读写组件。这同时也是中心化管理所需要的资源,不然中心化管理没法做。

ruoyi的权限系统设计相当于把这两个设计聚合到一起。它解决了jwt过期的缺点,但jwt的优点完全没有用到,且其他的缺点无法解决,反而事倍功半。

我们的后台管理会采用token+redis这种方案。因为作为管理系统,我们必须要对会话进行管理。jwt这种方案更适合应用在其他的场景,比如邮件验证等。

接口鉴权

接口鉴权是使用spring security的@PreAuthorize+权限编码进行鉴权。这样设计鉴权逻辑可以有很大的自由,且可以精细到对每个接口单独进行权限管理。

image-20210215103340240

image-20210215103405036

这一块我们采用和ruoyi相同的方案。

RBAC角色权限设计

关于RBAC的方面可以参考《RBAC权限系统分析、设计与实现》 或自行百度,这里我们不再赘述。

ruoyi也是采用的RBAC的设计方案,做到用户-角色-权限三者的解耦。不过它实现的有一些小瑕疵,并未完全做到用户与权限的解耦。

image-20210215211158151

在这里,它创建用户的时候,不是将角色放在用户里,而是直接将权限放在用户里。这样会导致角色权限更新的时候要去更新所有用户缓存的权限,或者让具有此角色的用户重新登录才能获取到最新的权限list。前者需要遍历所有用户,对缓存的压力极大,后者会使token集体失效,对登录接口的压力极大。

image-20210215212949736

ruoyi为了解决它,只更新了当前用户的权限缓存。但随之而来的是:如果其他用户具有此角色,则这个用户必须要重新登陆才能使用最新添加的权限,否则他还是没有这个权限。这样就造成了数据不一致。

这个问题有更好的解决办法。在我们的系统中,用户信息存储的是角色信息,而非具体的权限信息。我们在访问接口时,会拿着用户的当前角色去缓存中查询角色对应的权限集合进行鉴权;当角色权限更新时,只要更新角色对应的权限集合及缓存。不需要进行用户遍历,也不会有数据一致性的问题。我们只要付出一次redis查询的代价就可以解决。

动态数据源

动态数据源这里要思考两处:一是怎么创建多个DataSource,多个DataSource的配置如何维护;二是在使用时如何选择自己想要的数据源,即数据源选择。

数据源创建

在数据源创建时,创建主从数据源(实际上主从只是个代号,你也可以叫一号数据源二号数据源等等)。@ConfigurationProperties这个注解可以将对应配置下的属性自动填充到bean中;DruidProperties是统一进行druid连接池的配置。这样会创建两个数据源。

image-20210215223545825

image-20210215223757427

image-20210215224336965

在创建完主从数据源后,会将两个数据源bean都加入到dynamicDataSourcebean中,这个bean需要继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,她内部会维护一个map,在数据源切换时会根据标记从map取出数据源使用。

image-20210215223810748

image-20210215225013355

注意上图的这个方法,这里是选择最终调用的数据源,ruoyi是使用ThreadLocal(线程变量)的方式进行选择。image-20210215225426832

还有一点要说明。如果你使用这种客制化数据源,一定要将spring boot数据源的自动配置类排除。不然会因无法找到数据源配置而启动失败。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

数据源选择

在选择数据源时,ruoyi是用AOP+ThreadLocal做的数据源选择。

整体思路:

image-20210218000912204

使用示例:

image-20210215231153555

需要注意,因为数据源切换时基于aop的,所以同类调用方法不能使用this调用,需要使用代理对象调用,即AopContext.currentProxy()

数据权限隔离

数据权限隔离,或者说叫数据权限分配,他的业务目的是让每个人只能看自己全权限的数据,不能越权去看别人的数据。比如每个销售员自己的单子都不想给别人看,但是销售经理应该可以看到所有销售员的单子,这时候就有了它的用武之地。

主流做法

目前这块主流的解决方案都是拼sql,将权限配置保存在数据库中,只不过是最后拼在哪:有些设计选择拼在sql最后,由开发人员显示的调用;也有些设计是在表名处做文章,根据权限条件限制生成临时表。

ruoyi的数据权限隔离的做法便是前者,且是基于aop做的。根据方法上标记@DataScope注解,在当前方法拦截并生成权限sql片段,最后由开发人员显式的插入。具体片段生成方法见com.ruoyi.framework.aspectj.DataScopeAspect#dataScopeFilter

image-20210217231043061

因数据权限这里每种业务要求都不相同,这个没法做一个非常通用的方案,我们这里也是会维持相同的设计。

代码生成器

代码生成器这里算是比较重要的一块,一个好用的生成器可以极大的减少工作量。

这里技术不是难点,会用模板引擎就ok,比如freemarker,velocity等。ruoyi使用的是velocity,它性能要比freemarker好一些,语法友好度也好一些。具体语法可以参考文档介紹| Velocity 中文文档 - wizardforcel,这里不做赘述

代码结构和预留结构是重点。直接影响你是否需要加班,像ruoyi自带的就有增删查改,导出,权限,这一块就做的很好,极大的减轻了工作量。

在这里提一下数据库乐观锁的问题,因为是管理系统,一条数据可能被很多人同时修改,所以需要加入乐观锁控制数据版本,保证所见即所得。

我们这里因为使用框架的原因,需要对生成器模板做一些魔改。还有权限方面,我们希望做成自动放入数据库的模式,不要手动插入;还会在表设计中加入乐观锁。

common工具

工具包这里就比较自由了,常见的包有apache-common,guava,hutool,objectMapper等。这里我们主要分析工具类的设计原则。

设计原则

  1. 尽量静态

    工具类的方法应尽可能做成静态方法和静态字段,如果无法做成静态方法,则要考虑将这个类做成单例模式,不要多次实例化。因为静态方法会直接放在jvm栈中,效率要比非静态的高,而且对于开发人员来说也用着舒服。

  2. 命名规范

    一般情况下,可以叫做xxxUtil或xxxUtils,如果有一些业务领域特定的工具类,则应该叫做xxxHelper,比如redisHelper,exportHelper等。且这些helper因为都要基于某一个组件的原因,必须要实例化。所以需要做单例设计。

  3. 性能要求

    工具类方法作为基础架构的一种,调用的频率非常之高,所以内部实现在设计时就要考虑时间复杂度和空间复杂度,一点点性能问题就会被十倍百倍的放大,这也是一些公司要求工具类必须要技术经理编写的原因。

  4. 接口规范

    一是方法命名要通俗易懂,且参数数量尽量控制不要超过5个。不然参数太多,命名只有自己看得懂,太麻烦了没人愿意用的。

    二是是弃用原则。弃用时如果项目已经发布,要将原有方法打上@Deprecated标记,并记录推荐使用的新方法。原有方法千万不要立刻删除,要在写一个新接口。因为老方法不知道有多少个人用,一旦删除别人的系统可能立刻崩溃。在打上标记后再经历几个版本迭代,发个删除通告,再删就可以了。

其他

其实ruoyi的common包我看着很难受,他把core包的内容也放到了common包中,实际上core包和common包应该是独立的两个包。common负责工具,core负责核心配置。像RuoYiConfigBaseControllerBaseException这些应该集成在core包中单独发布。而像常量类、utils应放在common中。没必要混合在一起。这里感觉ruoyi比较混乱。

关于这部分我会拆成两个包:core和common包,让职责划分更加清晰一点。

其他设计

定时任务

定时任务这里ruoyi是对quartz做了一个包装,封装了一些查询任务信息的方法和执行任务,具体可以参考quartz的方法。

如果对job的需求比较轻量可以试一下这个工具全局定时任务-CronUtil,非常轻量

异步日志

这里也是用aop做了一个异步日志的拦截,核心就收集入参、出参、用户信息等信息调用异步线程插入数据库,比较简单。

总结

其实后端设计主要就是几块:数据库设计,配置设计,工程结构设计。剩下的基本都是经验积累:比如怎么去设计代码生成器,工具类应该怎么设计,这里只能讲一些原则性的思路(劝告?),还是要多听多写多看多读源码。

关于ruoyi的后端部分分析就到此结束了,有一些设计原则和优化的点我都在相应章节记录下来。等我们正式开始编码的时候会一起应用。


从绫开始的后台管理系统(二)
https://note.0moe.cn/后台管理系统/2021/02/14/从绫开始的后台管理系统(二)/
作者
Dawn_南风
发布于
2021年2月14日
许可协议