blog/content/post/奇技淫巧/前端读写OSS.md
2025-02-20 13:03:00 +08:00

214 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 前端读写阿里云OSS的两种临时授权方式
description: Node.js服务利用阿里云STS token以及签名URL获取临时访问凭据在前端实现安全资源读写
date: 2025-02-18T20:31:00+08:00
slug: ossfront
categories:
- 奇技淫巧
tags: [
"前端",
"后端",
"JavaScript",
"Node.js",
"STS token",
"OSS",
"建站",
"签名URL"
]
lastmod: 2025-02-19T01:43:00+08:00
---
在网站开发中经常需要用户读写资源到对象存储如阿里云OSS尤其是更加敏感的上传操作我们不可能把bucket设置为公共写权限。比如上传头像、投稿文件等常见的做法是后端接收资源并将其上传到阿里云OSS但这样一来如果是大文件会严重占用后端服务器的带宽和存储空间并且由于服务器的带宽限制用户上传大文件时会非常慢。
为了解决此问题阿里云给我们提供了两种方式让我们选择在前端完成将文件上传到OSS。我们知道如果直接把永久AccessKeyId和AccessKeySecret暴露在前端那么后果不堪设想。因此我们使用下面两种安全的方式来实现临时授权。
1. 使用STS token来实现临时授权。
2. 使用签名URL来实现临时授权。
# 使用STS token
> **官方文档**
> - [使用STS临时访问凭证访问OSS](https://help.aliyun.com/zh/oss/developer-reference/use-temporary-access-credentials-provided-by-sts-to-access-oss)
阿里云STS token提供了一个接口我们能够获取一个临时的AccessKeyId和AccessKeySecret这个凭据的权限是受限的能够访问到有限的资源并且有明确的过期时间这样我们就可以在前端安全地完成文件上传并且不用担心资源被滥用。
首先,我们到阿里云控制台的[RAM访问控制](https://ram.console.aliyun.com/users)创建一个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角色的权限。
<img src="https://cdn.ember.ac.cn/images/bed/202502190028468.png" width="600">
然后我们新建一个RAM角色可信实体选择**阿里云账号**角色名我们命名为submission表示用户读写文件专用的角色。
<img src="https://cdn.ember.ac.cn/images/bed/202502190033599.png" width="600">
然后我们进入左边的“权限策略”新建一个自定义策略假设我们要允许此角色读写Put和Get`mybucket`这个bucket下的`a/``b/``c/`这三个目录下的文件,那么我们就可以新建一个策略,内容如下:
```json
{
"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
<img src="https://cdn.ember.ac.cn/images/bed/202502190042428.png" width="600">
后端接口中我们就可以使用STS服务
```js
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
});
});
```
前端可以调用这个接口,获取临时凭证,然后使用这个凭证来完成文件的上传和下载。
```html
<!-- 需要先引入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下载文件](https://help.aliyun.com/zh/oss/user-guide/how-to-obtain-the-url-of-a-single-object-or-the-urls-of-multiple-objects)
> - [使用签名URL上传文件](https://help.aliyun.com/zh/oss/user-guide/upload-a-file-using-a-file-url)
签名URL是OSS提供的一种临时授权方式后端服务器只需要完成请求的身份验证然后返回一个签名URL前端就可以直接利用这个URL在前端完成文件的上传和下载而不需要通过后端中转文件。
后端接口中我们只需要完成请求的身份验证然后返回一个签名URL。注意你的accessKey用户需要具有bucket的相关操作权限。
```js
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]`。
然后前端就可以轻松地完成文件的上传和下载了。
```html
<script>
const { url } = await result.json();
const downloadFile = await fetch(url); // 下载文件
const uploadFile = await fetch(url, {
method: 'PUT',
body: file
}); // 上传文件,生成签名时要使用'PUT'方法
</script>
```