文件上传是常见需求,一般我们不会把文件直接上传到应用服务器,因为单台服务器存储空间是有限的,不好扩展。
我们会用单独的 OSS (Object Storage Service)对象存储服务来上传下载文件。
比如一般会买阿里云的 OSS 服务。
我们本地文件存储是目录-文件的组织方式:

而 OSS 服务的存储结构是这样的:

一个桶里放一些文件。
阿里云 OSS 的控制台也提到了对象存储没有目录层级结构:

但下面明明是支持目录的呀:

这其实只是模拟实现的。
Object 会存储 id、文件内容、元数据三部分信息:

阿里云 OSS 只是用元信息部分模拟实现了目录。
就像打了个 tag 一样,并不是说文件存储在这个 tag 下,只是你可以用这个 tag 来检索文件。

除了对象存储 OSS,阿里云也提供了文件存储和块存储的方式:

块存储就是把整块磁盘给你用,你需要自己格式化,存储容量有限。
文件存储就是有目录层次结构,你可以上传下载文件,存储容量有限。
对象存储就是 key-value 存储,分布式的方式实现的,存储容量无限。
这些简单了解就行,绝大多数情况下,我们都是用 OSS 对象存储。
我们买个阿里云的 OSS 服务来试试看:

我买了 40G 的 OSS 国内通用资源包,花了 5 块钱。
然后我们创建个 Bucket(桶):

在北京创建了一个 Bucket,文件就会存储在那里的服务器上。
设置公共读,也就是这些文件大家都可以直接访问。
不然私有的方式,你访问每个文件都要带上一些身份信息:

有的同学说,不是静态文件要在全国各地都能访问到么?存在北京的服务器会不会访问速度慢?
这是 CDN 的活:

接入 CDN 后,访问该域名会走到云服务的 DNS,然后返回一台最近的缓存服务器的地址,这台服务器会从源站拿文件来缓存,之后就不再访问源站。
这里的源站就可以是 OSS 服务。

创建 Bucket 之后,我们上传个文件试试:

上传完之后在文件列表就可以看到这个文件了:

点开可以看到文件详情,用这个 URL 就可以访问:

当然,生产环境下我们不会直接用 OSS 的 URL 访问,而是会开启 CDN,用网站域名访问,最终回源到 OSS 服务:


在控制台里上传很简单,那如果想在代码里上传呢?
官方文档里有示例代码,我们试试看:

bash
mkdir oss-test
cd oss-test
npm init -y

安装用到的包:
npm install ali-oss
写下代码:
javascript
const OSS = require('ali-oss')
const client = new OSS({
region: 'oss-cn-beijing',
bucket: 'guang-333',
accessKeyId: '',
accessKeySecret: '',
});
async function put () {
try {
const result = await client.put('cat.png', './mao.png');
console.log(result);
} catch (e) {
console.log(e);
}
}
put();
region 在概览里可以看到:

这里的 accessKeyId 和 acessKeySecret 是什么呢?
本来我们身份认证都是通过用户名密码:

但这样不够安全,所以我们创建了 accessKey 用来代表身份,用它来做身份认证,就算泄漏了,也不影响别的:

我们创建个 accesKey:


创建完成后,拿到 accesKeyId 和 accessKeySecret 后运行代码:

这里的 mao.png 是这样的:

在控制台可以看到上传成功了:

这就是 OSS 用 api 上传文件的用法。
只是我们刚刚用的 accessKey 不够安全。
打开 accessKey 管理页面的时候就提示了:

让我们不要直接用 accessKey,而是创建一个子用户再创建 accessKey。
那我们就创建个子用户:


然后用这个 accessKey 的 id 和 secret 就好了:

但你直接换上它还不行:

会提示你 403,没有权限。
需要你授权下:

新增一个授权:

把 OSS 的管理和读取权限给这个子用户:

然后再试下:

这时候就上传成功了。
回过头来看下,不得不说阿里云在安全这一块设计的就很巧妙。
如果我们直接用用户名密码验证呢:

那万一泄漏了不就完蛋了么?
但是如果创建个 accessKey 用它来做身份认证:

就算泄漏了我也可以禁用啊:

再进一步,直接用这个 accessKey 它是有所有权限的。
我们先创建个 RAM 子用户,再分配给他某些权限,这样就算泄漏了,是不是能做的事情就更少了?
当然就更安全。

所以说,阿里云这套 accessKey 和 RAM 子用户的身份认证方式,还是很不错的。
再说回 OSS,一般的文件直接上传就行,涉及到大文件就要分片上传了。
分片上传实现原理是前端常见面试题了,大家都能答上来。
就是把文件用 slice 方法分成一个个小的片,然后全部上传完之后请求一个接口合并分片。
阿里云的大文件分片上传也是这样实现的:

具体怎么用直接看文档就好了,这里我们就不试了:

有了 OSS 服务之后,我们上传文件还需要经过应用服务器么?
可以经过也可以不经过。
如果经过应用服务器,那就要客户端上传文件之后,我们在服务里接受文件,上传 OSS:

这样当然是可以的,还能保护 accessKey 不被人窃取。
只是会浪费应用服务器的流量。
那如果不经过呢?

在客户端用 accessKey 把文件传到 OSS,之后把 URL 传给应用服务器就好了。
这样减少了应用服务器的流量消耗,但是增加了 accessKey 暴露的风险。
各有各的坏处。
那有没有啥两全其美的办法呢?
有。

阿里云的文档里也提到了这个问题。
它给出的解决方案就是生成一个临时的 accessKey 来用。
代码是这样的:
javascript
const OSS = require('ali-oss')
async function main() {
const config = {
region: 'oss-cn-beijing',
bucket: 'guang-333',
accessKeyId: 'LTAI5tDemEBPwQkTx65jZCdy',
accessKeySecret: 'I0vHYOoqIC78lH7A5c5XB1H7Pev7bp',
}
const client = new OSS(config);
const date = new Date();
date.setDate(date.getDate() + 1);
const res = client.calculatePostSignature({
expiration: date.toISOString(),
conditions: [
["content-length-range", 0, 1048576000], //设置上传文件的大小限制。
]
});
console.log(res);
const location = await client.getBucketLocation();
const host = `http://${config.bucket}.${location.location}.aliyuncs.com`;
console.log(host);
}
main();
上传 OSS 的地址,用的临时 accessKey 和 signature 和 policy 都有了:

这些代码不用记,文档里都有:

这样就能在网页里用这些来上传文件到 OSS 了:
创建个 index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@1.6.5/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file"/>
<script>
const fileInput = document.getElementById('fileInput');
async function getOSSInfo() {
await '请求应用服务器拿到临时凭证';
return {
OSSAccessKeyId: 'LTAI5tDemEBPwQkTx65jZCdy',
Signature: 'NfXgq/qLIR2/v87j/XC9sjrASOA=',
policy: 'eyJleHBpcmF0aW9uIjoiMjAyNC0wMS0yMFQwMzoyNjowOC4xMDZaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF1dfQ==',
host: 'http://guang-333.oss-cn-beijing.aliyuncs.com'
}
}
fileInput.onchange = async () => {
const file = fileInput.files[0];
const ossInfo = await getOSSInfo();
const formdata = new FormData()
formdata.append('key', file.name);
formdata.append('OSSAccessKeyId', ossInfo.OSSAccessKeyId)
formdata.append('policy', ossInfo.policy)
formdata.append('signature', ossInfo.Signature)
formdata.append('success_action_status', '200')
formdata.append('file', file)
const res = await axios.post(ossInfo.host, formdata);
if(res.status === 200) {
const img = document.createElement('img');
img.src = ossInfo.host + '/' + file.name
document.body.append(img);
alert('上传成功');
}
}
</script>
</body>
</html>
这里 getOSSInfo 应该是请求服务端的接口,拿到刚才我们控制台输出的那些东西。
这里就简化下,直接写死在代码里了。
引入 axios,用这些信息来上传文件。
跑个静态服务器:
vbscript
npx http-server .

这时候你上传文件的时候会提示跨域错误:

我们在控制台开启下跨域:

然后再试下:

上传成功了!
控制台文件列表也可以看到这个文件:

这就是完美的 OSS 上传方案。
服务端用 RAM 子用户的 accessKey 来生成临时 accessKey,然后返回给客户端,客户端用这个来直传文件到 OSS。
因为临时的 accessKey 过期时间很短,我们设置的是一天,所以暴露的风险也不大。
这样服务端就根本没有接受文件的压力,只要等客户端上传完之后,带上 URL 就好了。
案例代码上传了 github:github.com/QuarkGluonP...
总结
上传文件一般不会直接存在服务器目录下,这样不好扩展,一般我们会用阿里云的 OSS,它会自己做弹性扩展,所以存储空间是无限的。
OSS 对象存储是在一个 bucket 桶下,存放多个文件。
它是用 key-value 存储的,没有目录的概念,阿里云 OSS 的目录只是用元信息来模拟实现的。
我们在测试了在控制台的文件上传,也测试过了 node 里用 ali-oss 包来上传、在网页里直传 OSS 这三种上传方式。
不管在哪里上传,都需要 acessKeyId 和 acessKeySecret。
这个是阿里云的安全策略,因为直接用用户名密码,一旦泄漏就很麻烦,而 acessKey 泄漏了也可以禁用。而且建议用 RAM 子用户的方式生成 accessKey,这样可以最小化权限,进一步减少泄漏的风险。
客户端直传 OSS 的方式不需要消耗服务器的资源,但是会有泄漏 acessKey 的风险,所以一般都是用服务端生成临时的 acessKey 还有其他信息,然后用这些信息来上传。
这种方案就是最完美的 OSS 上传方案了。
掌握了这些,就完全足够应对工作中的 OSS 使用了。