EL-ADMIN学习笔记
一,支持接口限流,避免恶意请求导致服务层压力过大
常见的限流功能一般有两个关注点:
1.限流原则,即以什么样的条件对请求进行识别以及放行。常见的作法是给予每个调用API的系统不同的唯一编码,用于监控某一编码的调用是否超出上限。
2.限流机制,即通过什么样的机制实现限流。常见的作法是通过Redis中Key的TTL失效机制来限制访问频率。
比较常见的处理方案是使用Redis中Key的TTL失效机制来限制访问频率,然后通过切面实现处理限流逻辑,并使用自定义注解将零散的关注点集中起来。
接下来按照上述两个关注点对EL-ADMIN中如何实现的接口限流进行拆解:
首先是它的限流原则:
图一,限流使用实例
通过me.zhengjie.modules.system.rest.LimitController的test方法就可以看到EL-ADMIN对限流的封装还是比较易用的,只需通过注解设置指定时间段内允许访问的次数以及限流的键名(前缀+key),其中name是指的该接口的功能备注,并不会影响限流的功能,只是方便在日志中检索对应接口的信息。
图二,限流切面
但是不止于此(见上图),进一步挖掘处理这个注解的Around类型切面(me.zhengjie.aspect.LimitAspect)的时候发现限流的原则不止是通过key的方式还有通过客户端IP的方式来限制,Around切面可以理解为在所有Limit注解处添加了一个执行拦截器,判断是否满足允许执行的条件后通过joinPoint.proceed()执行原来的方法(这里类似拦截器的过滤链处理模式)。总结如下:在使用Redis作为限流核心机制的设计方案中,Key的设置直接决定了限流原则。
其次是它的限流机制:
通过图二的限流切面分析可知,相比于一般的限流逻辑,EL-ADMIN进一步对Redis的操作做了优化,使用了Lua脚本来执行限流的判断机制。具体的限流机制是通过将接口地址结合Limit注解的prefix+key+url的方式构建了一个接口对应的唯一键,通过查询Redis上该键TTL(生命周期period)时间段内的值(访问次数count)实现限流的逻辑。限流机制这部分的核心Lua实现如下图所示,需要注意的是在第6行和第7行,在Redis中假如直接对一个key执行incr命令会将这个key设置为1。
图三,限流机制Lua脚本
另外相比一般场景下使用RedisTemplate执行这一系列操作,使用Lua脚本操作Redis的优势如下:
1.保证对Redis操作的原子性,防止出现对同一个key的非原子性操作。
2.减少网络数据传输节省带宽。
二,支持接口级别的功能权限与数据权限,可自定义操作
这里提到的功能权限一般是指使用者能否使用系统的功能,数据权限同理则是指使用者能否访问对应的数据,如果看过上一篇《RuoYi-Vue学习笔记-分析》就不难理解这里的数据权限的使用场景了。
一般的功能权限都是基于RBAC思想设置一套基于角色的权限授予方案,让用户的权限通过角色这一层抽象和系统中的权限发生交互。
常见的功能权限的解决方案是使用Spring Security + Jwt Token前者提供了成套的权限过滤器,只需要将我们设计权限系统的判断逻辑接入过滤器即可,后者则有效的解决了客户端身份盗用以及服务端的横向扩展的问题。数据权限在使用MyBatis框架时可以通过超类冗余字段+请求处理切面实现,在使用Spring Boot Jpa中的具体实现可以探索一番。在这一章节我们可以拆解以下两个功能的具体实现:
1.Spring Boot Jpa的数据权限解决方案的实现。
2.基于Spring Security框架实现的功能权限如何实现全局接口自定义权限放行,即通常情况下需要使用@PreAuthorize("hasAnyRole('admin','menu:edit')")这类注解才能让接口放行,但是超管可以访问所有的接口,那么我们就要给每个接口都添加admin的注解值了,这样做是效率很低的,通过全局接口自定义权限放行的方案即可实现在一处配置即可全局接口免去这个配置(这一特性的具体实现在之后的第三章节进行解构)。
首先,一起来看看EL-ADMIN对数据权限的封装,在拆解这部分之前需要先对Jpa有个大概的了解,不然会对这部分实现拆解的理解会有影响。当下常见的应用访问数据的方式有以下两种,一种是以Hibernate为首的信奉"程序员就不该写SQL"的一众ORM框架,这类框架不会让程序员写一行SQL,统统都交给框架处理,与之对应的则是封装了SQL变化的一众注解以及类(个人感觉这个抽象层的机制比SQL还难...);一种是以MyBatis为代表的通过JDBC桥接了数据库,让程序员能够灵活的自定义SQL语句来执行,这类框架则是注重灵活,与之对应的则是需要有一定的SQL基础。
JPA就是Hibernate所遵守的标准名称,使用这个标准的框架可以通过以下列表的注解实现SQL语句的对等功能。
图四,JPA注解及其功能
参考RuoYi对数据权限的处理,要想实现数据权限至少有两个点需要关注,
一是查询前用户数据权限的获取;
二是查询时数据权限注入;
首先是用户数据权限的获取,这部分数据一般是通过登录的时候放置在用户的cookies中或是登录后的session中,这部分在EL-ADMIN中是通过下图中的几步实现的,需要注意的是使用了Vuex的异步接口请求的功能(图中第一步)。
图五,前端登录流程
至此,系统已经获取到了用户的数据权限的信息,系统前端请求接口的时候携带对应的用户Token即可,后端接口在登录的时候已经对Token和用户信息(包含了用户数据权限记录)进行了绑定和Redis缓存,便于接口调用的时候直接通过缓存获取。接下来就是第二步查询时数据权限注入的拆解,这部分RuoYi是使用了注解+切面+超类基本和业务代码不侵入的方式实现的,EL-ADMIN的实现对比之下和业务代码的耦合性较大(这个锅其实和JPA的设计哲学“程序员绝不写SQL”有关,也不能让EL-ADMIN背)。如下图所示,自定义数据权限注解在这个功能中的职责一如既往的是用来实现查询条件以及字段的个性化的注入到查询过程中。
图六,后端数据权限查询拼接流程
上图中需要注意的有这么两点:
1.左侧line62,数据权限信息获取中是通过Spring Security提供的用户信息容器SecurityContextHolder获取的,可以理解为通过线程池绑定了用户信息构成的上下文。
2.右侧line45同理,也是通过1中的这个上下文获取的用户对应的数据权限信息。
三,自定义权限注解与匿名接口注解,可快速对接口拦截与放行
这个特性其实是特性二的补充,相当于在接口前做了一个切面,针对特定的角色可以不去做权限校验。针对这个特性,一般的解决方案是通过注解对接口的权限进行标记,然后通过切面/拦截器/过滤器对接口调用的请求进行匹配判断。至此引出这一特性的几个关注点:
1.用于标记拦截以及放行的自定义注解
2.整合入Spring Security的切面/过滤器/拦截器
首先关注拦截以及放行的注解,这部分标记权限拦截的注解使用了常见的@PreAuthorize,只不过其中的值是使用了"@el.check('****')"这种形式的字符串。这部分是通过SpringEL表达式获取了容器中的Bean,然后计算对应的结果,这部分逻辑可以参考原始的hasAnyRole方法返回的结果来实现的。究其本质就是针对权限判断这部分,框架其他的部分不做变更,添加一个模块输入输出的类型不变,那么就是可以契合原有框架的,通过下图的原生的写法比对更好理解一些,可以看到两者的返回类型都是布尔值,且根据面向接口编程的规范我们甚至可以实现一个该方法所在的接口实现类来定制我们自己的权限判断逻辑。
图七,自定义权限判断规则
放行的则是使用了自定义的注解,值得一看的是EL-ADMIN通过对Spring MVC的@RequestMapping注解的扩展实现了框架需要的功能,充分的体现了六大设计原则之一的开闭原则。如下图所示,放行的注解有以下两种使用场景,一是针对现有代码直接添加注解即可,一是针对新的方法可以把对应的注解当作SpringMVC框架原生的注解使用。
图八,自定义注解的放行策略
自定义注解实现的放行策略这部分需要注意的有几处:
1.通过图八中的方法,通过覆盖原有的注解实现带有权限放行+路由功能的自定义注解。
2.自定义权限放行和Spring Security框架的整合,通过SpringMVC中提供的Bean收集所有带有放行注解的路由实现无配置自动化的放行。
3.放行所有的Options为HTTP请求动词的请求,这是因为跨域请求需要用到这类动词的请求,拦截就无法实现跨域了。
四,前后端统一异常拦截处理,统一输出异常,避免繁琐的判断
一般来说统一的异常拦截都可以通过过滤器或是拦截器实现,Spring很贴心的帮我们做了这部分的封装,这一章节主要是拆解前后端结合的异常拦截的工程实践方案。
1.首先需要在前端对响应有全局的处理逻辑,让所有的响应都能通过前端逻辑进行转化提示,说白了就是axios做的事情了。
2.然后在后端通过过滤器,切面,拦截器等等实现对响应的修改操作。
这里主要对后端的实现做拆解,常见的异常处理机制都是在业务层、控制层抛出,通过@ExceptionHandler注解处理异常抛出的过程。该机制的实现是通过切面实现的,其中切面的切点声明是通过注解@RestControllerAdvice或@ControllerAdvice实现。
基于切面,这部分功能特性可以做的拓展有异常处理(注解@ExceptionHandler),初始化数据绑定器用于特定数据格式的参数绑定(注解@InitBinder),特殊请求参数的绑定(注解@ModelAttribute)。对应的使用场景分别是,根据抛出的不同的异常封装不同的请求流转以及响应,将前端请求的字符串类型的日期转化为Date类型的日期,不通过控制器层将参数绑定到Model中。
总之,将@ControllerAdvice这个注解理解为在过滤器,拦截器,之后的最后一道拦在请求和我们应用的处理逻辑之间的处理流程即可。通过这个切面结合注解@ExceptionHandler可以将控制器层以及业务层抛出的所有异常根据异常类型,异常所属的包进行差异化的拦截处理。
五,支持运维管理,可方便地对远程服务器的应用进行部署与管理
这个特性已经让EL-ADMIN脱离了后端框架的定位了,虽然这个功能还是比较常用的,但是这个特性中包含的功能只要单独拿出来,然后稍微拓展就可以当作服务器管理工具来用了。
通过官方文档的描述可以把重点放在这几个功能的实现上:
新增服务器,整合ssh客户端连接服务器。
新增应用,通过ftp等命令将应用发送至服务器指定目录。
部署应用,通过shell命令实现应用的部署,启动等操作。结合2后还可以执行上传的sql脚本。
通过下图九可以更好的理解上述的特性点,首先是第一步,用户通过本地和EL-ADMIN的交互将应用包上传到服务器上,此时应用文件会通过相关的配置参数推送到远端的服务器,然后通过部署脚本进行部署。这里比较关键点是需要能够监控远端服务器此时指定的应用的状态,一般使用微服务架构的应用可以使用服务注册与发现中心来实现这个监控,如果是单体的巨石应用则可以监控对应的端口进程状态即可。
图九,用户对远程服务器应用部署与管理
上述几个功能用到了两个开源组件以及一个之前所没有接触过的技术点,分别是:
jsch,连接指定的机器后执行shell命令,并获取对应的返回值。
ganymed-ssh2,连接指定的机器后获取文件或上传文件的工具。
websocket,这个技术的确是做CURD Boy不经常接触的技术点,定义是这样描述的:
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
这里有两个点需要解释一下,一个是单个,怎么理解这个单个呢,一般来说你在网页上点击一个超链接就是单个的概念了,一个请求就是单个TCP连接了。其次就是全双工的定义,这是一个通讯概念,同属一类的有单工,半双工,全双工。他们的区别就是从两个维度上来区分,其一是信息的传递是单向的还是双向的,单向的就是单工,双向的就是双工。其二是发送和接受能否同时执行,能够就是全双工,不能就是半双工。对应我们实际在开发中使用到的通信技术和协议,http,ajax这些就是半双工的,因为请求时信息的流向时双向的,但是不能同时进行接受信息和发送信息,websocket则是提供了一种在发送消息的时候接受消息的技术标准。
有了websocket这个新玩具, EL-ADMIN把它用到了远端服务器状态的实时信息推送,比如应用部署完成,应用卸载,应用当前运行状态的获取等等。在广阔的业务场景中,因为websocket的实时性比ajax要高的多,所以也可以用到聊天的场景中。
参考文献:
[0]EL-ADMIN特性https://el-admin.vip/guide/kslj.html#%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B
[1]Lua语法https://www.runoob.com/lua/lua-basic-syntax.html
[2]Redis中使用Lua脚本https://www.cnblogs.com/kaituorensheng/p/11098194.html
[3]JPA常见注解https://blog.csdn.net/wang_1220/article/details/107915815
[4]数据权限https://www.cnblogs.com/anderson-question/articles/14907553.html