在网站开发中经常需要用户读写资源到对象存储(如阿里云OSS),尤其是更加敏感的上传(写)操作,我们不可能把bucket设置为公共写权限。比如上传头像、投稿文件等,常见的做法是后端接收资源并将其上传到阿里云OSS,但这样一来,如果是大文件,会严重占用后端服务器的带宽和存储空间,并且由于服务器的带宽限制,用户上传大文件时会非常慢。
为了解决此问题,阿里云给我们提供了两种方式,让我们选择在前端完成将文件上传到OSS。我们知道,如果直接把永久AccessKeyId和AccessKeySecret暴露在前端,那么后果不堪设想。因此,我们使用下面两种安全的方式来实现临时授权。
- 使用STS token来实现临时授权。
- 使用签名URL来实现临时授权。
使用STS token
官方文档
阿里云STS token提供了一个接口,我们能够获取一个临时的AccessKeyId和AccessKeySecret,这个凭据的权限是受限的(能够访问到有限的资源),并且有明确的过期时间,这样我们就可以在前端安全地完成文件上传,并且不用担心资源被滥用。
首先,我们到阿里云控制台的RAM访问控制,创建一个RAM用户。
这里解释一下RAM用户和角色的区别:
- RAM用户:是RAM的一种实体身份类型,有确定的身份ID和身份凭证,通常与某个确定的人或应用程序一一对应。RAM用户由阿里云账号(主账号)或具有管理员权限的其他RAM用户、RAM角色创建,创建成功后归属于该阿里云账号
- RAM 角色:是一种虚拟用户,可以被授予一组权限策略。与RAM用户不同,RAM角色没有永久身份凭证(登录密码或访问密钥),需要被一个可信实体扮演。扮演成功后,可信实体将获得RAM角色的临时身份凭证,即安全令牌(STS Token),使用该安全令牌就能以RAM角色身份访问被授权的资源。
RAM角色适用于移动应用直传等场景,服务端下发临时安全令牌(STS Token),客户端通过临时安全令牌进行资源直传,避免服务端中转带来的多余开销。
― 阿里云官方文档
创建用户后,记下这个用户的AccessKeyId和AccessKeySecret,然后,我们为这个用户添加权限策略,我们选择AliyunSTSAssumeRoleAccess,这个权限是必须的,赋予用户扮演RAM角色的权限。
然后,我们新建一个RAM角色,可信实体选择阿里云账号,角色名我们命名为submission,表示用户读写文件专用的角色。
然后,我们进入左边的“权限策略”,新建一个自定义策略,假设我们要允许此角色读写(Put和Get)mybucket
这个bucket下的a/
、b/
、c/
这三个目录下的文件,那么我们就可以新建一个策略,内容如下:
1{
2 "Version": "1",
3 "Statement": [
4 {
5 "Effect": "Allow",
6 "Action": [
7 "oss:PutObject",
8 "oss:GetObject"
9 ],
10 "Resource": [
11 "acs:oss:*:*:mybucket/a/*",
12 "acs:oss:*:*:mybucket/b/*",
13 "acs:oss:*:*:mybucket/c/*"
14 // 如果要允许读写目录本身,则需要添加:
15 // "acs:oss:*:*:mybucket/a/",
16 // "acs:oss:*:*:mybucket/b/",
17 // "acs:oss:*:*:mybucket/c/"
18 ]
19 }
20 ]
21}
创建完成后,我们回到角色管理,为这个角色添加刚刚创建的策略,资源级别选择“账号级别”。然后,记下这个角色的ARN,即:
后端接口中,我们就可以使用STS服务:
1const OSS = require('ali-oss');
2const STS = require('ali-oss').STS;
3const express = require('express');
4const app = express();
5
6app.use(express.json());
7const sts = new STS({
8 accessKeyId: 'yourAccessKeyId',
9 accessKeySecret: 'yourAccessKeySecret',
10 // 填入刚刚获得的RAM用户的AccessKeyId和AccessKeySecret
11});
12
13app.get('/get-sts', async (req, res) => {
14 // 完成用户权限验证,如通过token获取id
15 const userId = req.user.id;
16
17 const result = await sts.assumeRole(
18 'acs:ram::10**********:role/submission', // 填入刚刚获得的RAM角色的ARN
19 '', // 可选,填入自定义策略,最终角色获得的权限是此策略和RAM角色的权限的交集
20 900, // 填入临时凭证的有效期,单位为秒,最小值是900秒,最大值是控制台给角色设定的最大会话时间
21 'session' + userId // 填入一个自定义的会话名称。当使用相同的会话名调用assumeRole方法时,新的会话将立即生效,旧的会话将被终止。所以,使用userId等标识来区分不同的会话是很好的做法。
22 );
23 // 返回临时凭证
24 res.json({
25 accessKeyId: result.Credentials.AccessKeyId,
26 accessKeySecret: result.Credentials.AccessKeySecret,
27 securityToken: result.Credentials.SecurityToken,
28 expiration: result.Credentials.Expiration
29 });
30});
前端可以调用这个接口,获取临时凭证,然后使用这个凭证来完成文件的上传和下载。
1<!-- 需要先引入ali-oss -->
2<script src="https://unpkg.com/ali-oss"></script>
3<script>
4const { accessKeyId, accessKeySecret, securityToken, expiration } = await result.json();
5
6async function getSTSToken() {
7 // 前端身份验证操作,如通过token获取id
8 const result = await fetch('/get-sts');
9 return result.json();
10}
11
12const client = new OSS({
13 region: 'oss-cn-hangzhou', // 填入你的bucket所在的地域
14 accessKeyId: result.accessKeyId, // 临时凭证的AccessKeyId
15 accessKeySecret: result.accessKeySecret, // 临时凭证的AccessKeySecret
16 stsToken: result.securityToken, // 临时凭证的SecurityToken
17 bucket: 'mybucket', // 填入你的bucket名称
18 endpoint: 'https://yourdomain.com' // 如果有自定义域名就填
19 cname: true, // 如果使用自定义域名,则需要设置为true
20 secure: true, // 如果强制https,则需要设置为true
21 refreshSTSToken: async () => {
22 const refreshToken = await getSTSToken();
23 return {
24 accessKeyId: refreshToken.accessKeyId,
25 accessKeySecret: refreshToken.accessKeySecret,
26 securityToken: refreshToken.securityToken,
27 }
28 }, // 自动刷新临时凭证的函数
29 refreshSTSTokenInterval: 1000 * 60 * 5 // 设置临时凭证的刷新时间,单位为毫秒,刚刚我们设置了900秒,这里可以设置得短一些,为5分钟
30});
31
32client.put('a/example.file', file); // 上传文件
33client.get('a/example.file'); // 下载文件
34client.put('d/example.file', file); // 如果访问权限外的目录,会报403错误
35</script>
使用签名URL
官方文档
签名URL是OSS提供的一种临时授权方式,后端服务器只需要完成请求的身份验证然后返回一个签名URL,前端就可以直接利用这个URL在前端完成文件的上传和下载,而不需要通过后端中转文件。
后端接口中,我们只需要完成请求的身份验证,然后返回一个签名URL。注意,你的accessKey用户需要具有bucket的相关操作权限。
1const OSS = require('ali-oss');
2const express = require('express');
3const app = express();
4
5app.use(express.json());
6const client = new OSS({
7 region: 'oss-cn-hangzhou', // 填入你的bucket所在的地域
8 accessKeyId: 'yourAccessKeyId', // 填入你的AccessKeyId
9 accessKeySecret: 'yourAccessKeySecret', // 填入你的AccessKeySecret
10 bucket: 'mybucket', // 填入你的bucket名称
11 endpoint: 'https://yourdomain.com', // 如果有自定义域名就填
12 cname: true, // 如果使用自定义域名,则需要设置为true
13 secure: true, // 如果强制https,则需要设置为true
14 authorizationV4: true // 按阿里云官方文档,使用V4签名
15});
16
17app.get('/get-url', async (req, res) => {
18 // 完成用户权限验证,如通过token获取id
19 const userId = req.user.id;
20
21 const { fileKey } = req.query;
22 const url = await client.signatureUrlV4(
23 'GET', // 设置请求方法
24 20, // 设置签名URL的有效期,单位为秒
25 {
26 headers: {
27 // 设置请求头部信息
28 },
29 queries: {
30 // 设置请求参数
31 'response-content-disposition': 'attachment' // 如果要强制浏览器下载,则需要设置此参数
32 // 还可以自定义文件名
33 // 'response-content-disposition': 'attachment; filename="example.file"'
34 }
35 },
36 fileKey // 填入文件路径
37 );
38 res.json({ url });
39});
注意:
signatureUrlV4
方法并不是返回一个URL,而是返回一个包含URL的Promise对象。我一开始没用await
,结果导致返回了一个空对象{}
,在URL中呈现的是[Object Object]
。
然后前端就可以轻松地完成文件的上传和下载了。
1<script>
2 const { url } = await result.json();
3 const downloadFile = await fetch(url); // 下载文件
4 const uploadFile = await fetch(url, {
5 method: 'PUT',
6 body: file
7 }); // 上传文件,生成签名时要使用'PUT'方法
8</script>