第十章:应用-灵析社区

懒人学前端

一、如何实现大文件上传?

如果我们想要实现大文件上传,最简单的思路就是把大文件切成小文件,然后确保小文件都上传给后端,后端再把小文件拼装起来。具体到每个步骤,我们都需要保证结果的正确性并考虑性能。

大文件上传步骤如下:

  • 读取文件
  • 文件切片
  • 上传文件

我们分别讲解各个实现步骤:

1. 读取文件

我们先用 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];
});

2. 文件切片

文件信息包含文件的名字、大小、文件类型等信息。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;
}

上面的代码中:

  • A 处:

file 是获取到的大文件

size 这里我们设置每个小切片的大小为 1 * 1024 * 1024

  • B 处:slice() 方法帮助我们将大文件切成多个小文件

我们把大文件按照每个 1 * 1024 * 1024 大小进行切割,调用 createChunk(files) 后, 切割好的小文件列表就可以上传到服务器上了。

如果需要优化,可以将计算部分使用 Web Worker 计算。

3. 上传文件

我们点击“上传”按钮之后,将文件上传到服务器。我们先给按钮添加事件:

upload.addEventListener("click", () => {
	const uploadList = chunkList.map(({ file }, index) => ({
		file,
		size: file.size,
		chunkName: `${files.name}-${index}`,
		fileName: files.name,
		index
	}));
	uploadFile(uploadList);
});

接下来,我们就需要实现 uploadFile() 函数。我们需要把文件切片列表变成 FormDataFormData 表示表单数据的键值对,我们将切片使用这种格式上传到服务器。创建表单类型数据:

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 的大小重新排列, pid 为 0 的是根,包含其他所有项目
  • 新增 children 属性

同一个 pid 的项目都放在 children 数组中

如果 pid 下无项目,则 children 为空

接下来,我们就来实现这个数状结构。主要步骤如下:

  • 遍历 flatList,把数据转成 Map 结构,方便查找
  • 同时,直接从 Map 中查找相同的 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 存入,首次遍历的时候只有一个元素,即根节点(pid0)
  • 从数组 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 系统维护用户信息。流程如下:

首次登录时:

  • 用户登录某应用网站,浏览器将用户的登录重定向到 SSO 认证中心
  • SSO 进行检查和校验是否有现有的 SSO Cookie
  • 由于首次登录,并且用户浏览器不存在 SSO Cookie,因此请求用户使用 SSO 配置的连接登录。
  • 用户登录后,将设置 SSO Cookie,用户将被重定向到应用程序,并使用包含用户相关的身份信息的令牌(Token)

后续登录中:

  • 应用网站将用户重定向到 SSO 认证中心
  • SSO 认证中心 检查是否存在现有的 SSO Cookie
  • SSO 认证中心验证 SSO Cookie 是否有效

无效,用户被重定向到应用网站的登录页面

有效,用户将被重定向到应用程序,并带有包含用户相关身份信息的令牌(Token)。

其实,SSO 认证中心相当于一个登录中介,它统一管理用户在多系统下的登录操作。

使用 SSO 的好处就是简化登录流程,用户友好且安全。

四、Web 常见的攻击方式有哪些?

常见的 Web 攻击方式有以下几种:

  • 跨站脚本攻击(XSS 攻击)
  • 跨站请求伪造(XSRF 攻击)
  • SQL 注入

XSS 攻击

MDN 定义如下:

跨站脚本攻击(Cross-site scripting,XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。

简单来说,跨站脚本攻击利用恶意脚本发起攻击,通常这些恶意脚本可以任意读取 cookie、session tokens,或其他敏感网站信息。以下两种情况容易发生 XSS 攻击:

  • 从一个不可靠的链接进入到一个 Web 应用程序。
  • 没有过滤掉恶意代码的动态内容被发送给 Web 用户。

如果要过滤恶意代码,提交给后端,就能尽可能避免此类攻击。

DOM 中的内联事件监听器,如 locationonclick 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。

如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就能在前端 render 阶段避免 innerHTMLouterHTML 的 XSS 隐患。

XSRF 攻击

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。

如果你登录了 bank.com 网站,此时,你有了来自该网站的身份验证 cookie。浏览器将会在你的每次请求上带上 cookie,以便识别你,执行所有的敏感财务操作。

当你在另一个窗口中浏览网页时,不小心访问了一个 eval.com 的网站,该网站有一段提交表单的 JavaScript 代码: <form action="https://bank.com/pay">,它含有交易字段。这时候,提交表单请求虽然由 eval.com 发起,当它携带了 bank.comcookie,因此验证通过,攻击就完成了。

那么我们怎么预防呢?方案有:

  • 阻止不明外域的访问

同源检测

Samesite cookie

  • 提交时要求附加本域才能获取的信息

CSRF Token

双重 cookie 验证

SQL 注入

SQL 注入攻击,是通过将恶意的 SQL 查询或添加语句插入到应用的输入参数中,再在后台 SQL 服务器上解析执行进行的攻击。比如,后端使用 SQL 查询时,篡改的查询参数或其他 SQL 代码,就会造成 SQL 注入攻击。

可以使用预编译方式或者 ORM 框架避免此类攻击发生。

阅读量:660

点赞量:0

收藏量:0