前言
在各位看官开始阅读前,请注意以下须知:
- 本文在实现学成在线项目时,不是根据视频一步一步进行,而是总览需求文档,根据自身关注点,挑选重点着重讲解与实现,其他部分则会通过自研templates一步完成
- 本文会对一些关注点进行“魔改”,魔改的出发点有两个:1.增加难度;2.效果更好
本文的目录结构如下:
目录
言归正传,接下来开始介绍学成在线项目的实现
1. 基础实现部
1.1 项目框架搭建
项目采用(父工程(基础工程+聚合服务工程))的三层结构,父工程统合版本信息,基础工程实现全局依赖功能,聚合服务工程针对特定功能组做聚合与整体打包(内部还可继续细分子工程,但仅包含一个启动类),我们以仅包含内容管理聚合服务的图解释该三层模型:
我们以下图解释聚合服务工程:
聚合服务工程将MVC架构具象化为三个子工程,分别是映射数据库的数据模型工程,依赖于数据模型工程执行业务的业务工程,与前端交换数据的接口工程(依赖前两个工程),并建立一个聚合工程统一管理三个模块,体现在pom文件里如下:
<modules>
<module>xuecheng-plus-content-api</module>
<module>xuecheng-plus-content-model</module>
<module>xuecheng-plus-content-service</module>
</modules>
聚合服务工程仅在接口工程下建立SpringBoot的启动类,业务工程与数据模型工程通过jar包依赖注入接口工程。由于聚合服务工程均在统一文件结构下,因此接口工程的启动类扫描时,可以将业务工程与数据模型工程的Bean注入到容器中
这里额外说明一个知识,@SpringBootApplication默认扫描启动类所在包及其下属包,如果要添加其他的路径,须通过以下声明@SpringBootApplication(scanBasePackages = “com.xxx”)
1.2 数据库建立
直接拿项目给的sql生成
1.3 基本功能一键生成
利用自定义模板统一生成了dao, service, api层代码,然后放在各自的模块下
1.4 单元测试(从service再到api)
首先需要明确,dao不需要单元测试,因为dao只存放持久层模型,即po,没有业务逻辑所以不需要测试
service有两部分需要单元测试,一种是由模板一键生成的mapper业务,测试其对数据库的基本访问是否完备;一种是人工编程的service业务,测试其对数据的处理与返回是否合适
api主要测试其对数据的处理与返回,测试分两步进行:
- 使用Spring自带的单元测试主要测试的是接口的处理逻辑
- 测试返回给前端的数据是否合适:
- 使用HTTP-Client工具测试返回给前端的数据是否合适(该测试方法全面且简便)
- 也可以使用Swagger文档测试,Swagger文档使用时需要注意,其无法解析泛型类,无法解析与测试以Map收集的参数。因此,利用Swagger文档测试接口时,如果是以路径请求参数,必须分开导入,如果是以Body请求参数,必须创建对象接收。像下面这样的扩展性强(不限制传参个数)的接口定义就只能利用HTTP-Client测试:
public Results GetCourseBasePageList(@RequestParam Map<String,Object> params)
1.5 前后端联调
- 跨域是什么:跨域是浏览器禁止访问非同源资源(同源即协议、主机、端口完全一致)
- 跨域的三种解决方案:
- 使用JSONP访问
- 服务端添加请求头
Access-Control-Allow-Origin:*
,可通过Spring提供的CorsFilter
过滤 - 浏览器请求同源nginx服务器,由nginx发放静态资源以及代理对服务端的访问
2. 扩展实现部
扩展实现部分主要介绍我比较感兴趣的,以前没有接触到的一些技术选型,不会涉及项目实现的所有内容
2.1 媒资管理
媒资管理只负责文件(图片、视频等)的上传与下载接口开发,以及维护文件信息表(该表一般不向外暴露接口,而是由内部的上传下载接口做访问与修改)
媒资管理主要使用到的中间件是Minio,其相对于其他分布式文件系统的优势:
- 开源
- 通过纠删码实现自修复
- 去中心化设计
2.1.1 Minio恢复数据原理浅析
这里简要介绍一下纠删码自修复的作用:纠删码自修复即对于一个分为n块的数据,增加m块校验,这n+m块数据丢失任意的m块,剩下的n块都可以将其恢复为原来的n+m块数据
那么纠删码中的RS(Reed-Solomon)里德-所罗门类纠删码的实现原理是什么?
- 维护一个(n+m)(n)的矩阵B,其上部分是单位阵,下部分是范德蒙或柯西矩阵(用于满足任意nn矩阵可逆),该矩阵是固定形式,不随分块内容变化。根据B*D计算得到验证块
- 当任意m块丢失时,利用B`可逆原则,根据图4可恢复原D块数据
2.1.2 断点续传
先总结一下教程中给出的断点续传流程:
- 前端校验得到原始文件的MD5值,然后调用分片算法,将原始文件分为若干分片
- 依序调用后端的checkChunk接口,接口参数
MD5值、分片名
,若检测到分片在Minio已存在,接口反馈对应信息,前端上传流程结束;否则调用后端的uploadChunk接口,接口参数MD5值、分片文件流、分片名
,上传后并不做校验,Minio返回True,后端就返回True - 当分片上传计数器达到目标值后,调用后端的mergeChunk接口合并分片,接口参数
MD5值、合并文件名、分片数
,合并上传后需下载源文件并临时存储在本地做MD5校验
上述流程中的问题主要有两方面:
- 完整性问题:每个分片并不会做校验,因此其完整性无法保证。此外,不完整分片也没有相关的清理逻辑,其将成为逃逸对象
- 冗余流问题:每次合并文件后的校验都需要先从Minio拉取合并文件至本地,该过程相对于整体流程是冗余的,仅仅是为了校验MD5就增加一条额外的开支,其必要性存疑
有没有更好的方案?
- 完整性问题的解决思路:Minio对于单体上传且大小<5G的文件采用MD5加密方式生成文件对象的etag,在Java中可通过
StatObjectResponse
对象拿到该etag,StatObjectResponse
对象相比于从Minio下载文件到本地来说是更为轻量级的选择。因此,对于分片存在性以及完整性的验证可依托于StatObjectResponse
对象构造快速验证逻辑,这里分享一个Minio的Java接口与简易demo的介绍文档 - 冗余流问题的解决思路:在保障了分片完整性的前提下,合并文件完整性在考虑到计算压力的前提下,我们可以对>=5G的文件采取容后处理方式,也就是不做即时验证,而考虑异步验证或惰性验证(即直到用户要求做验证时才做);而对于<5G的文件则仍旧利用
StatObjectResponse
对象的etag验证
该方案的细节如下:
- 前端校验得到原始文件的MD5值,然后调用分片算法,将原始文件分为若干分片,并校验得到各分片的MD5值
- 依序调用后端的checkChunk接口,接口参数
原始MD5值、分片名,分片MD5值
,通过原始MD5值确定文件路径,分片名确定特定文件,调用statObject
方法获取StatObjectResponse
对象以及对应etag,如代码块1所示,然后与分片MD5值比较,如相等,前端上传流程结束;否则调用后端的uploadChunk接口,接口参数原始MD5值、分片文件流、分片名
(该过程既完成了不存在时上传,也完成了分片文件不完整时直接覆盖) - 当分片上传计数器达到目标值后,调用后端的mergeChunk接口合并分片,接口参数
原始MD5值、合并文件名、分片数
,合并上传完成后,调用statObject
方法获取StatObjectResponse
对象以及对应size。当size<5G时,读取etag并与原始MD5值做比较判断合并文件完整性;当size>=5G时,考虑异步验证或惰性验证(即直到用户要求做验证时才做)
// 代码块1
// 获取StatObjectResponse对象
StatObjectResponse stat =
minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
// 读取etag
System.out.println(stat.etag());
// 读取size
System.out.println(stat.size());
2.1.3 任务调度中心
在我们的设想中,一个良好的任务调度中间件的最佳实践应该是:
- 封装性:封装为任务调度中心与执行器
- 执行器首先通过配置向任务调度中心自动注册并分组,使用者只需要关注执行器对单个任务的执行逻辑
- 任务调度中心提供以下API,使用者只需关注这两个API:
- 任务注册API:任务注册API应包含以下参数——1. 定时任务的cron语句,如果为空,则表示该任务为一次性任务;2. 执行器的分组;3. 执行器执行任务的参数(以Java对象形式发送);4. 任务优先级,注册成功后会返回一个任务流水号
- 任务删除API:任务删除API通过任务流水号精确定位待删除任务
- 除开放的API外,其他所有相关功能均由任务调度中心封装完成,使用者不用关心,包括:
- 执行器的调度逻辑:任务调度中心会对收集到的任务(有定时任务,也有一次性任务)依据优先级分配给设定分组下的执行器,执行器维护一个优先队列以缓存待处理任务
- 弹性扩容:任务调度中心读取未执行任务,并依据负载均衡原则向新执行器丢出任务以及通知对应执行器停止任务
- 故障恢复:对于宕机的执行器,任务调度中心会重新分配其未执行任务
- 流水号的唯一执行:执行某个流水号的执行器会持有当前流水号的行锁,只有拿到行锁的执行器才能执行对应流水号的任务(关键点,保障了即使在弹性扩容与故障恢复下,也是无重复执行的),并在执行完后更新状态字段
综上,一个封装良好的任务调度中间件的最佳实践可以很方便的使用:
- 通过配置注册执行器,编写单个任务执行逻辑
- 仅在微服务中向调度中心注册任务,其可靠性保证则完全交由调度中心实现
而XXL-JOB的封装思路无疑是有缺陷的,先讲讲它的优势:
- 可视化调度中心:
- 任务配置可视化:可以通过点选来完成cron语句的编写,实现全面的定时任务;也可以可视化的选择执行策略
- 执行器分组可视化
- Java语言与SpringBoot编写:
- 扩展性好,Java程序员可便捷地依托其主体进行扩展功能开发
再说说它的不足:
- 无法实现动态注册(即由微服务端向任务调度中心动态的注册任务)
- 无法实现优先级调度
- 流水号的唯一执行以及分布式任务分配的负载均衡需要手动实现,其没有封装对应功能
上述三大问题导致了XXL-JOB在使用时的不便捷,因此最佳实践中我也提出了相关的改动建议,如果后续有机会,我会基于XXL-JOB的源码进行上述修改
若不想修改源码,上述的不足则主要靠微服务端解决,其核心逻辑是在本地维护一个任务流水号表,微服务端向该表注册任务,并借由执行器实现逻辑完成优先级调度以及分片式负载均衡,借由锁实现流水号的唯一执行。而XXL-JOB通过定时任务定期调用执行器扫描流水号表并执行任务
2.2 认证授权
认证授权整个章节都是围绕oauth2协议展开的,所以我们有必要对oauth2协议的流程进行梳理,这里援引了教材中的图片,但其中有部分内容是有缺失的(已通过红箭头补充,部分内容来源于:知乎)
缺失的部分补上后,其后的流程大致如下所示:
- 由认证服务器通过重定向告知浏览器待请求的第三方业务回调接口以及授权码
- 第三方业务回调接口通过浏览器的重定向访问重新与浏览器建立会话,并根据授权码主动向认证服务器请求令牌,以及根据令牌向资源服务器请求用户信息,最终在一切完成后建立用户登录态,并通过会话告知浏览器用户登录态
这一流程决定了授权码这一步骤不可缺少:
- 由于第三方服务需要浏览器在认证成功后重新建立会话,以保证用户登录态可成功告知浏览器,因此就要求认证成功后需要重定向第三方业务的回调接口
- 由于重定向携带的信息在浏览器端有泄露风险,因此该请求不能携带令牌等关键信息,因此授权码作为中间信息提供给第三方服务以请求令牌,可以有效避免泄露带来的风险
2.2.1 jwt令牌简介
jwt令牌实现了服务端无状态认证,即服务端不再需要缓存任何用户信息相关的内容,而是由浏览器携带的jwt令牌传递,而jwt令牌则来自于服务端认证成功后颁发
jwt令牌的结构分为了三个部分:
{
块1:编码方式
}
{
块2:携带的用户数据
}
{
块3:encode(base64(块1)+'.'+base64(块2))
}
jwt无状态认证的关键在于服务端通过持有的密钥对经过base64编码的块1和块2再次进行加盐编码,保证块3无法被解析。因此,如果服务端收到的jwt令牌块3无法匹配块1和块2,说明其中某个部分被人为更改,从而拒绝该请求;此外,由于块1和块2的base64编码可解析,因此服务端对于认证成功的访问可直接经由解析块2从而获取到携带的用户信息
2.2 Spring Security启动类做了哪些事?
相信对Spring Security有简单了解的小伙伴都会有这个疑惑,相比于其他的启动类,Spring Security因为存在着跨服务的管理,所以其使用上更为繁琐:要想自定义配置生效,需要在所有被启动类管理的服务中都手动导入自定义配置文件
这为我们梳理Spring Security启动类完成的工作带来了困难,其实我们只需要把握两个启动Spring Security服务的注解,即可比较轻松的梳理出Spring Security的总体流程:
- @EnableWebSecurity
- @EnableResourceServer
@EnableWebSecurity
@EnableWebSecurity一般放在继承了WebSecurityConfigurerAdapter
的类上,以便SpringBoot启动时,通过WebSecurityConfigurerAdapter
所提供的默认过滤器链(或重写的过滤器链)来完成用户认证工作。有关于默认过滤器链(总计11个过滤器),可参考这篇博客的介绍,这里我们单列出里面重要的一个过滤器:UsernamePasswordAuthenticationFilter
,我们暂且称其为认证过滤器
该过滤器拦截对/login的post请求,并解析请求中的username和password,并通过一个重要的类AuthenticationManagerBuilder
来管理AuthenticationProvider
,从而进行用户的验证工作
因此,我们对验证工作的自定义,也是来源于对AuthenticationManagerBuilder
依赖的AuthenticationProvider
做覆写,从而将自定义的验证工具注入认证过滤器中
@EnableResourceServer
@EnableResourceServer则一般放在继承了ResourceServerConfigurerAdapter
的类上,其底层原理是为Spring Security的过滤器链再添加一个资源合法性校验过滤器
该过滤器拦截下符合拦截要求的请求后,依据TokenStore
类所提供的Token解析工具,获取到用户信息并读取其中的权限设置,然后再去拿到对应访问资源所应许的权限(由@PreAuthorize("hasAuthority('XXXPermission')")
指定),由此就可确定是否拒绝该次访问请求
因此,我们可以发现,Spring Securtiy所做的认证与授权工作,均可以由@EnableWebSecurity与@EnableResourceServer串联起来。注解前者后,Spring Securtiy默认的11个过滤器得以启动并组成过滤器链,从而自动完成包括:密码认证、登陆重定向,退出等等服务(业界通过覆写的方式重新定义默认实现);注解后者后,Spring Securtiy将在过滤器链中再添加一个资源合法性校验过滤器,用于授权实现