--- 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角色的权限。 然后,我们新建一个RAM角色,可信实体选择**阿里云账号**,角色名我们命名为submission,表示用户读写文件专用的角色。 然后,我们进入左边的“权限策略”,新建一个自定义策略,假设我们要允许此角色读写(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,即: 后端接口中,我们就可以使用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 ``` # 使用签名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 ```