本文目录
[[toc]]
断点续传
文件分片
文件本质上就是一串二进制内容,所以可以按照一定的规则切分,再逐个上传,每次保存哪些上传成功,哪些上传失败/还未上传即可。
大致流程如下:
- 将需要上传的文件分割为大小相同的数据块。
- 初始化上传任务,从后端获取本次任务的唯一 ID (也可以前端生成后发送后端),一般初始化还需要包括以下内容:
- 完整文件 hash/CRC 值: 用于校验最终合并后的文件是否完整。
- 文件分片数量或者所有分片 ID 合集: 让服务端知道有哪些分片会被上传。
- 通过串行/并行发送分片数据块,数据块包含以下内容:
- 任务 ID: 所属任务唯一 ID。
- 二进制内容: 文件在当前分片下的内容。
- 序号/ID: 标记当前为哪个分片。
- hash: 当前二进制内容的 hash 值,用于分片完整性校验。
- 服务端校验数据完整性,完整后合并为原文件。
断点续传
断点续传依赖于文件分片,未被分片的内容是无法续传的。
大致流程如下:
- 从服务端获取上传进度,还有哪些分片需要上传。
- 前端按照后端返回的未上传分片继续上传。
- 后端检测分片数量满足,从临时文件中读取文件合并,校验哈希值。
流式上传
在读取文件 buffer 的时候,边读边传,而不是读完再传。
需要注意的是 HTTP2 的流式传输可能会出现队头阻塞,推荐使用 HTTP3 实现。
其他注意点
- 上传失败自动重传,重传次数限制
- 上传过程中页面切走、断网、刷新导致异常中断,需要 recover
- 切片的时候按文件大小切还是按数量切?策略如何确定?
web worker后台上传- 秒传
- 创建任务时,发送了文件 hash ,后端比较是否已经存在,存在则不需要重复上传,即是秒传。
- 限制并发数避免浏览器/服务器占用过多连接数
- 切片动态调整?
- 定时任务清理文件碎片
示例代码
切片上传
const SIZE = 10 * 1024 * 1024 // 10MB
class fileUpload {
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = []
let cur = 0
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) })
cur += size
}
return fileChunkList
}
// 上传切片
async uploadChunks() {
const requestList = this.data
.map(({ chunk, hash }) => {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('hash', hash)
formData.append('filename', this.container.file.name)
return { formData }
})
.map(({ formData }) =>
this.request({
url: 'http://localhost:3000',
data: formData,
})
)
// 并发请求
await Promise.all(requestList)
// 合并切片
await this.mergeRequest()
}
async mergeRequest() {
await this.request({
url: 'http://localhost:3000/merge',
headers: {
'content-type': 'application/json',
},
data: JSON.stringify({
filename: this.container.file.name,
}),
})
}
async handleUpload() {
if (!this.container.file) {
return
}
const fileChunkList = this.createFileChunk(this.container.file)
this.data = fileChunkList.map(({ file }, index) => ({
chunk: file,
hash: `${this.container.file.name}-${index}`,
}))
await this.uploadChunks()
}
}web-worker 计算 hash
// web-worker 代码
// 库 spark-md5,它可以根据文件内容计算出文件的 hash
globalThis.importScripts('/spark-md5.min.js')
// 生成文件 hash
globalThis.onmessage = (e) => {
const { fileChunkList } = e.data
const spark = new globalThis.SparkMD5.ArrayBuffer()
let percentage = 0
let count = 0
const loadNext = (index) => {
const reader = new FileReader()
reader.readAsArrayBuffer(fileChunkList[index].file)
reader.onload = (e) => {
count++
spark.append(e.target.result)
if (count === fileChunkList.length) {
globalThis.postMessage({
percentage: 100,
hash: spark.end()
})
globalThis.close()
} else {
percentage += 100 / fileChunkList.length
globalThis.postMessage({
percentage
})
// calculate recursively
loadNext(count)
}
}
}
loadNext(0)
}// 和主线程的通讯
// 主线程使用 postMessage 给 worker 线程传入所有切片 fileChunkList,
// 并监听 worker 线程发出的 postMessage 事件拿到文件 hash
function calculateHash(fileChunkList) {
return new Promise((resolve) => {
this.container.worker = new Worker('/hash.js')
this.container.worker.postMessage({ fileChunkList })
this.container.worker.onmessage = (e) => {
const { percentage, hash } = e.data
this.hashPercentage = percentage
if (hash) {
resolve(hash)
}
}
})
}参考资料
前端鉴权
HTTP Basic Authentication ( 基本废弃 )
鉴权流程
---
title: HTTP Basic 鉴权流程
---
sequenceDiagram
participant 客户端
participant 服务器
客户端 ->> 服务器: 请求受限数据
服务器 ->> 客户端: HTTP/1.1 401 Unauthorized<br>WWW-Authenticate: Basic realm="baidu.com"
客户端 ->> 客户端: 弹框询问用户
客户端 ->> 服务器: 携带Base64认证头<br>Authorization: Basic Kitsid2FuZzp3YW5n==
服务器 -->> 服务器: 身份校验
alt 校验成功
服务器 -->> 客户端: 校验成功,返回状态码 200 资源
else 校验失败
服务器 -->> 客户端: 校验失败,返回 403 Forbidden
end优点
简单,广泛支持
缺点
不安全
- HTTP 未被加密,整个认证的账号密码是明文传输。
- 即使加密了,也可以通过重放请求获取权限。
- 难以防御中间人攻击。
cookie-session
鉴权流程
---
title: Cookie-Session 鉴权流程
---
sequenceDiagram
participant 浏览器
participant 服务端
participant User
participant Session
浏览器 ->> 服务端: POST 账号密码
服务端 ->> User: 校验账号密码
User -->> 服务端: 校验成功
服务端 ->> Session: 存 session
服务端 -->> 浏览器: Set-Cookie: sessionId
浏览器 ->> 服务端: 携带 cookie 请求接口
服务端 ->> session: 查 session 是否存在并且有效
session -->> 服务端: 校验成功
服务端 ->> 服务端: 接口处理
服务端 -->> 浏览器: 接口响应结果存储方式
服务端只是给 cookie 一个 sessionId ,而 session 的具体内容要自己存一下
- Redis(推荐):内存型数据库,以
key-value的形式存,正合sessionId-sessionData的场景;且访问快。 - 内存:直接放到变量里。一旦服务重启就没了
- 数据库:普通数据库。性能不高。
token
鉴权流程
---
title: Token 鉴权流程
---
sequenceDiagram
participant 浏览器
participant 服务端
participant User
浏览器 ->> 服务端: POST 账号密码
服务端 ->> User: 校验账号密码
User -->> 服务端: 用户数据
服务端 ->> 服务端: 生成 token
服务端 -->> 浏览器: Set-Cookie: token
浏览器 ->> 服务端: 携带 cookie 请求接口
服务端 ->> 服务端: 校验 token
服务端 ->> 服务端: 接口处理
服务端 -->> 浏览器: 接口响应结果问题
- 容易被篡改,所以需要添加 token 签名,也就是 JWT 机制
- 容易被盗用,所以有 refresh token
JWT
分为 3 部分: header 、 payload 、 signature
header 结构如下:
{
"typ": "JWT",
"alg": "使用的签名算法"
}payload 结构基本为自定义内容,会将部分公开信息保存在 payload 减少重复查询
{
"sub": "主题,一般是用户 ID",
"name": "用户名",
"role": "用户角色",
// 过期时间
"exp": 1627708800
}signature 一般就是纯字符串
三个部分均由 base64 编码,再通过 . 拼接而成
refresh token
refresh token 机制分为两部分:
access token: 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活refresh token: 用来获取access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的session一样处理
完整流程如下:
---
title: JWT 鉴权流程时序图
---
sequenceDiagram
participant 浏览器
participant 业务服务
participant 认证服务
participant User
rect rgb(222,227,232)
Note over 浏览器, User: 初次登录
浏览器 ->> 认证服务: POST 账号密码
认证服务 ->> User: 校验账号密码
User ->> 认证服务: 用户数据
认证服务 ->> 认证服务: 生成 refresh token (长期有效)
认证服务 ->> 认证服务: 生成 access token (短期有效)
认证服务 -->> 浏览器: refresh token, access token
end
rect rgb(222,227,232)
Note over 浏览器, User: access token 有效
浏览器 ->> 业务服务: 请求接口( access token )
业务服务 ->> 业务服务: 校验 access token (有效)
业务服务 ->> 业务服务: 接口处理
业务服务 -->> 浏览器: 接口响应
end
rect rgb(222,227,232)
Note over 浏览器, User: access token 失效
浏览器 ->> 业务服务: 请求接口( access token )
业务服务 ->> 业务服务: 校验 access token (失效)
业务服务 -->> 浏览器: 拒绝请求
浏览器 ->> 认证服务: refresh token
认证服务 ->> 认证服务: 校验 refresh token (有效)
认证服务 -->> 浏览器: access token (新)
浏览器 ->> 业务服务: 请求接口( access token (新))
业务服务 ->> 业务服务: 校验 access token (新)
业务服务 ->> 业务服务: 接口处理
业务服务 -->> 浏览器: 接口响应
end单点登录
---
title: 单点登录鉴权时序图
---
sequenceDiagram
participant User as 浏览器
participant AppA as 应用 A
participant AppB as 应用 B
participant SSO as SSO服务
rect rgb(232,244,232)
Note over User,SSO: 应用 A 引发登录
%% 登录失效
User ->> AppA: 访问应用 A
AppA ->> AppA: 凭证失效,需要登录
AppA -->> SSO: 重定向到 SSO
%% SSO 登录
User ->> SSO: 访问 SSO ,无 cookie
SSO ->> SSO: 校验 cookie 失败
SSO -->> User: 需要登录
User ->> SSO: 输入账号密码
SSO ->> SSO: 校验账号密码
SSO -->> User: URL 带 code 重定向到应用 A callback<br>种 SSO 域 cookie
%% SSO 同步权限状态到 AppA
User ->> AppA: 带 cookie 访问应用 A callback
AppA ->> SSO: 校验 code
SSO ->> SSO: 校验 code 成功
SSO -->> AppA: 换取 ticket
AppA -->> User: 重定向到应用 A 原页面,种 A 域 cookie ( ticket )
%% User 重新访问 AppA
User ->> AppA: 访问应用 A callback , 带 cookie ( ticket )
AppA ->> SSO: 校验 ticket
SSO ->> SSO: 校验 ticket 成功
SSO -->> AppA: 凭证有效
AppA ->> AppA: 处理业务
AppA ->> User: 业务响应
end
rect rgb(232,232,244)
Note over User,SSO: 应用 B 无需登录
%% 应用 B 还未同步 ticket
User ->> AppB: 访问应用 B
AppB ->> AppB: 凭证失效,需要登录
AppB -->> User: 重定向到 SSO
%% 从 SSO 同步登录状态
User ->> SSO: 访问 SSO ,带 SSO 域 cookie
SSO ->> SSO: 校验 cookie 成功
SSO -->> User: URL 带 code 重定向到应用 B callback ,种 SSO 域 cookie
User ->> AppB: 访问应用 B callback ,带 code
AppB ->> SSO: 校验 code
SSO ->> SSO: 校验 code 成功
SSO -->> AppB: 换取 ticket
AppB -->> User: 重定向到应用 B 原页面,种 B 域 cookie ( ticket )
%% User 重新访问 AppB
User ->> AppB: 访问系统 B ,带 B 域 cookie ( ticket )
AppB ->> SSO: 校验 ticket
SSO ->> SSO: 校验 ticket 通过
SSO -->> AppB: 凭证有效
AppB ->> AppB: 处理业务
AppB -->> User: 业务响应
endOAuth
---
title: OAuth 鉴权时序图
---
sequenceDiagram
participant User as 浏览器
participant Client as OAuth 客户端
participant AuthServer as OAuth 服务端
participant ResourceServer as 认证服务器
User ->> Client: 访问需要授权的资源
Client -->> User: 重定向到授权页面
User ->> AuthServer: 携带 client_id/redirect_uri/scope
AuthServer ->> User: 要求登录并授权
User ->> AuthServer: 输入凭证并授权
AuthServer -->> User: 重定向到 redirect_uri 携带 code
User ->> Client: 携带授权 code 访问
Client ->> AuthServer: 发送 code + client_secret 换取 access_token
AuthServer -->> Client: 返回 access_token
Client ->> ResourceServer: 携带 access_token 请求资源
ResourceServer ->> AuthServer: 验证 token 有效性
AuthServer -->> ResourceServer: 返回验证结果
alt 验证成功
ResourceServer -->> Client: 返回请求的资源
Client -->> User: 显示授权资源
else 验证失败
ResourceServer -->> Client: 返回 401 Unauthorized
Client -->> User: 显示错误信息
end
Note over Client,AuthServer: access_token 需要安全存储<br>并设置合理有效期