如果我们想要实现大文件上传,最简单的思路就是把大文件切成小文件,然后确保小文件都上传给后端,后端再把小文件拼装起来。具体到每个步骤,我们都需要保证结果的正确性并考虑性能。
大文件上传步骤如下:
我们分别讲解各个实现步骤:
我们先用 HTML
实现一个简单的文件上传界面:
HTML
代码如下:
<input type="file" id="input" />
<button id="upload">上传</button>
我们接下来需要实现读取文件:
let input = document.getElementById("input");
let upload = document.getElementById("upload");
// 创建一个文件对象
let files = {};
// 读取文件
input.addEventListener("change", (e) => {
files = e.target.files[0];
});
文件信息包含文件的名字、大小、文件类型等信息。File 对象部分属性如下图:
接下来创建切片:
function createChunk(file, size = 1 * 1024 * 1024) {
// A
const chunkList = [];
let cur = 0;
while (cur < file.size) {
chunkList.push({
file: file.slice(cur, cur + size) // B
});
cur += size;
}
return chunkList;
}
上面的代码中:
file 是获取到的大文件
size 这里我们设置每个小切片的大小为 1 * 1024 * 1024
我们把大文件按照每个 1 * 1024 * 1024 大小进行切割,调用 createChunk(files) 后, 切割好的小文件列表就可以上传到服务器上了。
如果需要优化,可以将计算部分使用 Web Worker 计算。
我们点击“上传”按钮之后,将文件上传到服务器。我们先给按钮添加事件:
upload.addEventListener("click", () => {
const uploadList = chunkList.map(({ file }, index) => ({
file,
size: file.size,
chunkName: `${files.name}-${index}`,
fileName: files.name,
index
}));
uploadFile(uploadList);
});
接下来,我们就需要实现 uploadFile()
函数。我们需要把文件切片列表变成 FormData
。FormData
表示表单数据的键值对,我们将切片使用这种格式上传到服务器。创建表单类型数据:
list.map(({ file, fileName, index, chunkName }) => {
const formData = new FormData();
formData.append("file", file);
formData.append("fileName", fileName);
formData.append("chunkName", chunkName);
return { formData, index };
})
发送上传文件请求,此处我们需要使用 Promise.all
来获取发送结果状态,如果每个请求都成功了将返回一个数组,如果其中一个抛出错误,将立即抛出错误。我们封装一个 uploadFile
函数:
async function uploadFile(list) {
const requestList = list
.map(({ file, fileName, index, chunkName }) => {
const formData = new FormData();
formData.append("file", file);
formData.append("fileName", fileName);
formData.append("chunkName", chunkName);
return { formData, index };
})
.map(({ formData, index }) =>
fetch(url, {
method: "POST",
body: formData
}).then((res) => {
console.log(res)
})
);
await Promise.all(requestList);
}
完整代码
HTML
部分:
<input type="file" id="input">
<button id="upload">上传</button>
JavaScript
部分:
let input = document.getElementById("input");
let upload = document.getElementById("upload");
let files = {};
let chunkList = [];
// 1. 读取文件
input.addEventListener("change", (e) => {
files = e.target.files[0];
chunkList = createChunk(files);
});
// 2. 创建切片
function createChunk(file, size = 1 * 1024 * 1024) {
const chunkList = [];
let cur = 0;
while (cur < file.size) {
chunkList.push({
file: file.slice(cur, cur + size)
});
cur += size;
}
console.log("chunkList", chunkList);
return chunkList;
}
// 3.文件上传
async function uploadFile(list) {
const requestList = list
.map(({ file, fileName, index, chunkName }) => {
const formData = new FormData();
formData.append("file", file);
formData.append("fileName", fileName);
formData.append("chunkName", chunkName);
return { formData, index };
})
.map(({ formData, index }) =>
// url 为请求地址
fetch(url, {
method: "POST",
body: formData
}).then((res) => {
console.log(res);
})
);
await Promise.all(requestList);
}
upload.addEventListener("click", () => {
const uploadList = chunkList.map(({ file }, index) => ({
file,
size: file.size,
percent: 0,
chunkName: `${files.name}-${index}`,
fileName: files.name,
index
}));
uploadFile(uploadList);
});
要实现两者的互相转换,我们先看一下两者的结构。扁平的数据内容如下:
const flat = [
{ id: 1, name: "部门1", pid: 0 },
{ id: 2, name: "部门2", pid: 1 },
{ id: 3, name: "部门3", pid: 1 },
{ id: 4, name: "部门4", pid: 3 },
{ id: 5, name: "部门5", pid: 4 }
]
数状结构:
const tree = [
{
id: 1,
name: "部门1",
pid: 0,
children: [
{
id: 2,
name: "部门2",
pid: 1,
children: []
},
{
id: 3,
name: "部门3",
pid: 1,
children: [
{
id: 4,
name: "部门4",
pid: 3,
children: [
{
id: 5,
name: "部门5",
pid: 4,
children: []
}
]
}
]
}
]
}
];
扁平结构转树状结构
观察两者的结构后,从扁平结构转树状结构需要做以下事情:
同一个 pid 的项目都放在 children 数组中
如果 pid 下无项目,则 children 为空
接下来,我们就来实现这个数状结构。主要步骤如下:
const flatToTree = (list) => {
const result = [];
const itemMap = {};
// 遍历
list.forEach((item) => {
const { id, pid } = item;
// children 不存在则添加 children
if (!itemMap[id]?.children) {
itemMap[id] = {
children: []
};
}
// 从 `Map` 中查找相同的 `pid` 项目,放在同一层 `children` 中
itemMap[id] = {
...item,
children: itemMap[id]["children"]
};
const treeItem = itemMap[id];
// 判断是不是根
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]?.children) {
itemMap[pid] = {
children: []
};
}
itemMap[pid]["children"].push(treeItem);
}
});
return result;
};
flatToTree(flat);
树状结构转扁平结构
树状结构转扁平结构只需要按照 id
排列即可,同时删除 children
属性。步骤如下:
queue
,将 data
存入,首次遍历的时候只有一个元素,即根节点(pid
为 0
)queue
头部取出元素,判断有无 children
属性有 children
,再次添加到 queue
的尾部,并删除 children
const treeToFlat = (data) => {
const result = [];
const queue = [...data];
// 遍历
while (queue.length) {
// 从数组头部取出数据
const node = queue.shift();
const children = node.children;
// 如果有 children, 继续添加到 queue 中
if (children) {
queue.push(...children);
}
// 删除 children 属性
delete node.children;
// 将处理好的节点添加到 result 中
result.push(node);
}
return result;
};
treeToFlat(tree);
单点登录(Single Sign On,简称 SSO)简单来说就是用户只需在一处登录,不用在其他多系统环境下重复登录。用户的一次登录就能得到其他所有系统的信任。
为什么需要单点登录
单点登录在大型网站应用频繁,比如阿里旗下有淘宝、天猫等,用户的一次操作或交易就可能涉及到其他众多子系统的协作。使用单点登录就可以让用户登录一次,免去频繁登录授权的苦恼。
早期的单系统登录
用户在登录界面输入自己的用户名和密码之后,浏览器向服务器发送登录请求,服务器验证通过用户信息后放入 session
,并将 sessionId
放入 Cookie
,随后返回给浏览器。登录过后的请求都将在 Cookie
中携带 sessionId
,服务器通过 sessionId
获取用户信息,最后返回响应信息。
单点登录的实现方式
如果一处登录的 session
能够共享,那么多个应用系统之间的登录状态也可以共享了。所以单点登录的关键在于,如何让 sessionId (或 Token)在多个域中共享。
同域下的单点登录
一个企业一般情况下只有一个域名,子系统使用二级域名。比如百度搜索是一级域名:https://www.baidu.com
,贴吧使用的是二级域名https://tieba.baidu.com
。如果我们要实现单点登录,可以将 Cookie
的域设置为顶域,即 https://www.baidu.com
,这样其余的子域系统都可以访问顶域的 Cookie
了。
当然,上面做法缺点也很明显,它不支持跨域。这还不是真正的单点登录。
要实现不同域下的单点登录, 我们需要一个 SSO 认证中心来专门处理登录请求。所有的请求(登录、退出、获取用户信息、当前用户状态)都请求 SSO 系统,SSO 系统维护用户信息。流程如下:
首次登录时:
后续登录中:
无效,用户被重定向到应用网站的登录页面
有效,用户将被重定向到应用程序,并带有包含用户相关身份信息的令牌(Token)。
其实,SSO 认证中心相当于一个登录中介,它统一管理用户在多系统下的登录操作。
使用 SSO 的好处就是简化登录流程,用户友好且安全。
常见的 Web 攻击方式有以下几种:
XSS 攻击
MDN 定义如下:
跨站脚本攻击(Cross-site scripting,XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。
简单来说,跨站脚本攻击利用恶意脚本发起攻击,通常这些恶意脚本可以任意读取 cookie、session tokens,或其他敏感网站信息。以下两种情况容易发生 XSS 攻击:
如果要过滤恶意代码,提交给后端,就能尽可能避免此类攻击。
DOM 中的内联事件监听器,如 location
、onclick
等,<a>
标签的 href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。
如果用 Vue/React
技术栈,并且不使用 v-html/dangerouslySetInnerHTML
功能,就能在前端 render
阶段避免 innerHTML
、outerHTML
的 XSS 隐患。
XSRF 攻击
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。
如果你登录了 bank.com
网站,此时,你有了来自该网站的身份验证 cookie
。浏览器将会在你的每次请求上带上 cookie
,以便识别你,执行所有的敏感财务操作。
当你在另一个窗口中浏览网页时,不小心访问了一个 eval.com
的网站,该网站有一段提交表单的 JavaScript 代码: <form action="https://bank.com/pay">
,它含有交易字段。这时候,提交表单请求虽然由 eval.com
发起,当它携带了 bank.com
的 cookie
,因此验证通过,攻击就完成了。
那么我们怎么预防呢?方案有:
同源检测
Samesite cookie
CSRF Token
双重 cookie 验证
SQL 注入
SQL 注入攻击,是通过将恶意的 SQL 查询或添加语句插入到应用的输入参数中,再在后台 SQL 服务器上解析执行进行的攻击。比如,后端使用 SQL 查询时,篡改的查询参数或其他 SQL 代码,就会造成 SQL 注入攻击。
可以使用预编译方式或者 ORM 框架避免此类攻击发生。
阅读量:660
点赞量:0
收藏量:0