4.0 遇到的问题
Jrebel 热更新很慢,原来是 resources 下的 rebel.xml 中的路径信息配置错了
验证码请求有问题,第一次打开前端页面的请求是正确的即
/api/checkCode
,但刷新页面后的请求就变成了/checkCode
为什么第一次打开是正确的?
第一次打开页面时,验证码请求可能是正确的,原因可能是:
浏览器缓存机制:第一次请求时,浏览器还没有缓存,所以直接请求了正确的 URL
Vue 组件的生命周期:在 onMounted 时,showPanel(1) 被调用,然后 restForm() 被调用,最后 changeCheckCode(0) 被调用,生成了正确的 URL
为什么刷新后丢失了 /api?
刷新后丢失 /api 前缀的可能原因:
浏览器缓存:浏览器可能缓存了之前的验证码图片 URL(没有 /api 前缀的版本)
Vue 响应式更新问题:在刷新时,Vue 的响应式系统可能没有正确更新 URL
组件重新初始化顺序问题:刷新时组件的初始化顺序可能影响了 URL 的生成
Bean 注入失败
原因分析
Spring 的依赖注入机制无法直接对 static 字段进行赋值。
即使使用了 @Value(“${ffmpeg}”),也无法将配置文件中的路径注入到静态字段中。
解决方案
使用非静态字段 + @PostConstruct 赋值给静态变量
4.1 文件下载
最简单的方案就是,后端直接把文件内容传给前端,然后前端通过某种方式(比如创建一个临时的 Blob 对象,再通过 URL.createObjectURL 生成一个下载链接)来保存到本地,确实是实现文件下载的一种最简单、最直接的方案。
前端发起请求: 前端告诉后端:“我要下载这个文件!”
后端读取文件: 后端找到对应的文件,然后把文件的所有内容(就像把一本书的内容全部读出来)都加载到内存里。
后端发送内容: 后端把这些文件内容一股脑地通过 HTTP 响应发送给前端。
前端接收内容: 前端接收到这些内容后,知道这是一份文件数据。
前端保存文件: 前端利用浏览器提供的能力,把这些数据保存成一个文件到用户的本地磁盘上。
在这个项目中,我们是没有做下载文件的登录鉴权的。原因如下:
部分浏览器可能会在下载模块集成第三方下载插件,比如迅雷。如果是通过迅雷进行下载,迅雷是拿不到我们的 session 的,也就是说第三方插件下载没有办法做接口鉴权。如果我们给下载文件接口做下载鉴权,可能会导致部分用户一直被拦截(迅雷无法传递 session)。
我们这个项目支持用户分享下载链接,也就是是说其他用户可以通过用户分享链接下载指定文件。如果我们要做登录鉴权,那么就必须想办法在下载连接中内置 session id,存在安全性问题。
基于以上两种原因,我们的下载文件接口实际上是没有做接口鉴权的。
但是如果简单的传递文件的地址就可以下载的话,就会出现安全性问题:用户只要知道一个文件的下载地址,就可以直接下载这个文件。
为了解决这个问题,我们把用户下载文件拆分成了两步:
前端传递文件 id 之后 后端查询该文件的地址,并且尝试构造下载 URL。
后端解析下载 URL,查询是否存在该文件,如果存在就下载。
构造下载 URL 的时候,我们也不会把文件的真实地址传过去,虽然这样最简单,但是存在安全性问题,因为暴漏了文件的真实地址。
生成下载 URL 逻辑:
根据文件 ID 和用户 ID 获取文件信息
如果文件信息为空、文件类型为文件夹,抛出业务异常
生成随机字符串作为下载码和文件路径
将下载码和 DownloadFileDto 对象保存到 Redis 中,多了层映射关系
文件下载逻辑:
根据下载码下载文件 该方法通过 HTTP 请求和响应,完成文件下载
根据下载码从 Redis 中获取文件下载信息
这样操作之后,即使恶意用户想要暴力破解 code,五分钟破解 50 位数字+字符也是有很大的难度的,避免了用户的越权下载。
获取文件路径和文件名
设置响应内容类型,指示浏览器以附件下载的形式处理响应
“application/x-msdownload”:表示浏览器应该将响应内容视为下载的文件。这个 MIME 类型通常用于触发浏览器的下载行为,而不是直接在浏览器中显示内容。
charset=UTF-8:指定响应内容的字符编码为 UTF-8,确保文件名等信息在传输过程中不会因编码问题导致乱码。
根据浏览器类型对文件名进行编码,以防止中文名乱码问题
设置响应头,指定文件下载的名称
总结一下文件下载的逻辑:
为了避免第三方下载和分享资源链接无法通过登录鉴权,我们拆分了下载文件这个接口。
把下载文件接口拆分成为:生成下载 URL 和根据 URL 下载指定文件两个接口。并且在生成下载 URL 的时候进行登录鉴权。
为了避免 URL 过于简单,可以被用户暴力破解,我们构造了一个 50 长度的字符串,以这个字符串为 Key,文件地址和文件名称为 value 存储在 Redis 中,并且设置有效期为五分钟。
当用户下载的时候,我们根据 URL 传递的字符串在 Redis 中读取指定的文件地址,进行下载。
但文件是直接一次传输的,而没有分片传输
4.2 文件删除(到回收站)
我们删除一个目录的时候,是删除这个目录以及其所有子目录和子文件。
在这个过程中,其实是对于一个目录的所有子目录和子文件是需要递归的查找
支持批量删除,所以无论前端传来的是个 fileId,还是 fileIds 列表,后端都走批量删除逻辑
ID 列表以“,”分割,先转化为数组
查找用户正在使用的文件列表
创建用于存储删除的文件或文件夹的 ID 列表
递归查找所有子文件夹的文件 ID,并添加到待删除的列表中
将目录下的所有文件更新为已删除
将选中的文件更新为回收站
4.3 物理删除
这里的删除其实只是把文件的地址从数据库中删除,但真正的文件还在服务器上保存的。主要原因如下:
我们在做秒传的时候,通过 MD5 值判断是否数据库中已有这个文件,如果已经有这个文件,就复制一份地址给这个用户,不用再上传当前用户的文件了。
这就会导致一份文件被多个用户实际共享。
如果删除是在服务器层面的话,那么 A 的删除文件会导致 B 对该文件的不可用。当然我们也有自己的解决策略:
- 其实就是在删除的时候在数据库层面搜索一下当前文件是否有被多个人共享,如果没有被多个人共享,就可以放心从服务器层面删除。
但我们这个删除逻辑只是从数据库层面删除,并没有从服务器层面删除。
逻辑流程:
看看是不是管理员(管理员删除是物理删除),不是则为 回收(删除该用户与该文件的信息)
递归查找所有子文件存入列表,然后统一删除所有子文件,再统一删除所选的文件(前端传来的文件 id)
这个删除过程有角色判断,决定删除标志,是管理员则改为物理删除
更新用户空间使用状况,存入 redis(统计数据库中所有 id 为当前用户的文件的大小,这就是用户当前使用的真实空间总大小)
4.4 恢复文件
其实这个后端接口的处理逻辑和文件的逻辑删除没有什么区别
逻辑删除是:找到当前目录的所有子文件和子文件夹,将其更新为“已删除”。
回收站恢复文件时:找到当前目录的所有子文件和子文件夹,将其更新为“正在使用”。
我们还需要注意:在恢复文件的时候,还需要检查待恢复文件与正在使用的文件是否存在同名冲突,如果有就要改名。
4.5 管理端 CRUD
管理员的权限主要有以下:
设置用户可使用空间大小。
设置所有的用户状态
查询所有的文件
查询所有的用户
删除指定用户的指定文件
下载任意文件
4.6 外端分享
用户的外端分享其实和我们的用户下载接口的基本逻辑一样。但需要注意的是:这里的外端分享一共要实现三个大功能:
允许所有用户直接通过分享链接进行下载
允许非当前文件分享的所有用户保存当前文件到自己的云盘中
1.创建分享文件接口:
就是创建一个 Fileshare 类,用来存储当前用户分享的文件。这里的 code 就是分享码,是由前端自己进行生成的。
我们在接口层构建完这个类之后,就需要把这个类交给 serve 层进行插入,再插入之前,我们还要根据用户要求构造过期时间,之后再进行插入。
2.构造分享链接:
这里我们沿用之前文件下载的思路,还是构造一个 code 来存储文件 id 和文件名称存入 redis。到时候按照用户的需求保存到云盘或者下载。这里调用下载时的生成 URL 的方法。
当我们的构造分享链接构造好时候,我们从这个链接点击进去,应该是先让我们展示分享的文件基本信息和校验验证码
因此还要有获取分享信息、以及校验接口
3.根据 shareid 查询分享文件简要信息
4.校验验证码:
检查分享码是否过期,以及更新浏览次数
之后就是 保存到自己云盘 或者 直接下载了
5.1 保存文件到自己的云盘
其实就是查找文件,拷贝地址给当前用户。唯一比较烦的就是:如果有层级关系,要拷贝层级关系。
我们看一看 serve 层,整层逻辑分为三部分:
保存文件到自己的云盘
如果有命名冲突就重命名
更新当前用户云盘空间