10 KiB
title | description | date | slug | categories | tags | lastmod | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
前端读写阿里云OSS的两种临时授权方式 | Node.js服务利用阿里云STS token以及签名URL获取临时访问凭据,在前端实现安全资源读写 | 2025-02-18T20:31:00+08:00 | ossfront |
|
|
2025-02-19T01:43:00+08:00 |
在网站开发中经常需要用户读写资源到对象存储(如阿里云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角色身份访问被授权的资源。
{{< quote author="阿里云官方文档">}} RAM角色适用于移动应用直传等场景,服务端下发临时安全令牌(STS Token),客户端通过临时安全令牌进行资源直传,避免服务端中转带来的多余开销。 {{< /quote >}}
创建用户后,记下这个用户的AccessKeyId和AccessKeySecret,然后,我们为这个用户添加权限策略,我们选择AliyunSTSAssumeRoleAccess,这个权限是必须的,赋予用户扮演RAM角色的权限。
然后,我们新建一个RAM角色,可信实体选择阿里云账号,角色名我们命名为submission,表示用户读写文件专用的角色。
然后,我们进入左边的“权限策略”,新建一个自定义策略,假设我们要允许此角色读写(Put和Get)mybucket
这个bucket下的a/
、b/
、c/
这三个目录下的文件,那么我们就可以新建一个策略,内容如下:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:PutObject",
"oss:GetObject"
],
"Resource": [
"acs:oss:*:*:mybucket/a/*",
"acs:oss:*:*:mybucket/b/*",
"acs:oss:*:*:mybucket/c/*"
// 如果要允许读写目录本身,则需要添加:
// "acs:oss:*:*:mybucket/a/",
// "acs:oss:*:*:mybucket/b/",
// "acs:oss:*:*:mybucket/c/"
]
}
]
}
创建完成后,我们回到角色管理,为这个角色添加刚刚创建的策略,资源级别选择“账号级别”。然后,记下这个角色的ARN,即:
后端接口中,我们就可以使用STS服务:
const OSS = require('ali-oss');
const STS = require('ali-oss').STS;
const express = require('express');
const app = express();
app.use(express.json());
const sts = new STS({
accessKeyId: 'yourAccessKeyId',
accessKeySecret: 'yourAccessKeySecret',
// 填入刚刚获得的RAM用户的AccessKeyId和AccessKeySecret
});
app.get('/get-sts', async (req, res) => {
// 完成用户权限验证,如通过token获取id
const userId = req.user.id;
const result = await sts.assumeRole(
'acs:ram::10**********:role/submission', // 填入刚刚获得的RAM角色的ARN
'', // 可选,填入自定义策略,最终角色获得的权限是此策略和RAM角色的权限的交集
900, // 填入临时凭证的有效期,单位为秒,最小值是900秒,最大值是控制台给角色设定的最大会话时间
'session' + userId // 填入一个自定义的会话名称。当使用相同的会话名调用assumeRole方法时,新的会话将立即生效,旧的会话将被终止。所以,使用userId等标识来区分不同的会话是很好的做法。
);
// 返回临时凭证
res.json({
accessKeyId: result.Credentials.AccessKeyId,
accessKeySecret: result.Credentials.AccessKeySecret,
securityToken: result.Credentials.SecurityToken,
expiration: result.Credentials.Expiration
});
});
前端可以调用这个接口,获取临时凭证,然后使用这个凭证来完成文件的上传和下载。
<!-- 需要先引入ali-oss -->
<script src="https://unpkg.com/ali-oss"></script>
<script>
const { accessKeyId, accessKeySecret, securityToken, expiration } = await result.json();
async function getSTSToken() {
// 前端身份验证操作,如通过token获取id
const result = await fetch('/get-sts');
return result.json();
}
const client = new OSS({
region: 'oss-cn-hangzhou', // 填入你的bucket所在的地域
accessKeyId: result.accessKeyId, // 临时凭证的AccessKeyId
accessKeySecret: result.accessKeySecret, // 临时凭证的AccessKeySecret
stsToken: result.securityToken, // 临时凭证的SecurityToken
bucket: 'mybucket', // 填入你的bucket名称
endpoint: 'https://yourdomain.com' // 如果有自定义域名就填
cname: true, // 如果使用自定义域名,则需要设置为true
secure: true, // 如果强制https,则需要设置为true
refreshSTSToken: async () => {
const refreshToken = await getSTSToken();
return {
accessKeyId: refreshToken.accessKeyId,
accessKeySecret: refreshToken.accessKeySecret,
securityToken: refreshToken.securityToken,
}
}, // 自动刷新临时凭证的函数
refreshSTSTokenInterval: 1000 * 60 * 5 // 设置临时凭证的刷新时间,单位为毫秒,刚刚我们设置了900秒,这里可以设置得短一些,为5分钟
});
client.put('a/example.file', file); // 上传文件
client.get('a/example.file'); // 下载文件
client.put('d/example.file', file); // 如果访问权限外的目录,会报403错误
</script>
使用签名URL
官方文档
签名URL是OSS提供的一种临时授权方式,后端服务器只需要完成请求的身份验证然后返回一个签名URL,前端就可以直接利用这个URL在前端完成文件的上传和下载,而不需要通过后端中转文件。
后端接口中,我们只需要完成请求的身份验证,然后返回一个签名URL。注意,你的accessKey用户需要具有bucket的相关操作权限。
const OSS = require('ali-oss');
const express = require('express');
const app = express();
app.use(express.json());
const client = new OSS({
region: 'oss-cn-hangzhou', // 填入你的bucket所在的地域
accessKeyId: 'yourAccessKeyId', // 填入你的AccessKeyId
accessKeySecret: 'yourAccessKeySecret', // 填入你的AccessKeySecret
bucket: 'mybucket', // 填入你的bucket名称
endpoint: 'https://yourdomain.com', // 如果有自定义域名就填
cname: true, // 如果使用自定义域名,则需要设置为true
secure: true, // 如果强制https,则需要设置为true
authorizationV4: true // 按阿里云官方文档,使用V4签名
});
app.get('/get-url', async (req, res) => {
// 完成用户权限验证,如通过token获取id
const userId = req.user.id;
const { fileKey } = req.query;
const url = await client.signatureUrlV4(
'GET', // 设置请求方法
20, // 设置签名URL的有效期,单位为秒
{
headers: {
// 设置请求头部信息
},
queries: {
// 设置请求参数
'response-content-disposition': 'attachment' // 如果要强制浏览器下载,则需要设置此参数
// 还可以自定义文件名
// 'response-content-disposition': 'attachment; filename="example.file"'
}
},
fileKey // 填入文件路径
);
res.json({ url });
});
注意:
signatureUrlV4
方法并不是返回一个URL,而是返回一个包含URL的Promise对象。我一开始没用await
,结果导致返回了一个空对象{}
,在URL中呈现的是[Object Object]
。
然后前端就可以轻松地完成文件的上传和下载了。
<script>
const { url } = await result.json();
const downloadFile = await fetch(url); // 下载文件
const uploadFile = await fetch(url, {
method: 'PUT',
body: file
}); // 上传文件,生成签名时要使用'PUT'方法
</script>