利用STS临时密钥服务快速搭建直传页面的实践
作者简介
吴硕卫:腾讯云技术支持工程师,现负责腾讯云存储产品的技术支持专项工作。
为了实现权限分离,提供更细颗粒度的权限控制,有效的控制帐号生效周期,本文通过腾讯云 CAM 产品的 STS(临时访问凭证)来实现部署,调试,验证等一系列的操作体验。
主要介绍基于腾讯云对象存储 COS,如何使用 COS 签名工具和 HTTP 请求工具 Postman 来验证临时密钥的有效性,以及如何快速实现一个 Web 端页面的文件直传功能。服务器上只需要生成和管理访问密钥,无需关心细节,文件数据都存放在腾讯云 COS 上。
在前端页面直接向 COS 发起请求,此时数据的上传和下载可以不经过后端服务器,既节约了后端服务器的带宽和负载,还可以充分利用 COS 的带宽和全球加速等能力,提升应用体验。
1、临时密钥
临时密钥(临时访问凭证) 是通过 CAM 云 API 提供的接口,获取到权限受限的密钥。
COS API 可以使用临时密钥计算签名,用于发起 COS API 请求。
COS API 请求使用临时密钥计算签名时,需要用到获取临时密钥接口返回信息中的三个字段,如下:
- TmpSecretId
- TmpSecretKey
- Token
2、使用临时密钥的优势
Web、iOS、Android 使用 COS 时,通过固定密钥计算签名方式不能有效地控制权限,同时把永久密钥放到客户端代码中有极大的泄露风险。如若通过临时密钥方式,则可以方便、有效地解决权限控制问题。例如,在申请临时密钥过程中,可以通过设置权限策略 policy 字段,限制操作和资源,将权限限制在指定的范围内。
有关 COS API 授权策略,请参见:
https://cloud.tencent.com/document/product/436/31923
- COS API 临时密钥授权策略指引
- 常见场景的临时密钥权限策略示例
3、架构说明
整体架构图如下所示:
其中:
- 用户客户端:即网页、用户手机 App 等。
- COS:腾讯云对象存储,负责存储 App 上传的数据。
- CAM:腾讯云访问管理,用于生成 COS 的临时密钥。
- 用户服务端:用户自己的后台服务器,这里用于获取临时密钥,并返回给网页。
(1) 用户客户端向用户的后台服务器请求临时密钥。
(2) 用户的服务器通过 CAM STS 接口请求临时密钥。
(3) CAM 返回临时密钥给用户服务器,该临时密钥有效期最长是 2 小时。
- 该接口属于 CAM 侧的,所以需要客户服务器有能够访问公网的能力。
- 该接口的 QPS 是 600,跟 COS 的存储桶 境内3W/境外3K QPS 不太相同。
- 用户不需要每次上传、下载操作都调用一次临时密钥 STS 接口,同一个临时密钥申请后在有效时间内都可以使用。
(4) 客户服务器下发临时密钥给客户端。
(5) 客户端获取到临时密钥的信息后,再做签名,携带签名请求上传、下载等操作。
4、环境准备
- 云服务器 1 台 -> 公网ip: 42.194.201.209
- Node.js、Git、NPM、Postman 最新版即可
本文测试使用的各个工具版本为:
名称 |
版本 |
---|---|
Node |
14.4.0 |
NPM |
6.14.5 |
Git |
1.8.3.1 |
一、部署临时密钥 STS 服务
COS 针对 STS 提供了 SDK 和样例,目前已有 Java、Nodejs、PHP、Python、Go 等多种语言的样例。具体内容请参见 COS STS SDK(https://github.com/tencentyun/qcloud-cos-sts-sdk)。各个 SDK 的使用说明请参见 Github 上的 README 和样例。
本次实践使用的是 Nodejs 语言。
# 拉取 COS STS SDK 代码:
git clone [https://github.com/tencentyun/qcloud-cos-sts-sdk.git](https://github.com/tencentyun/qcloud-cos-sts-sdk.git)
# 本次使用nodejs环境,进入到nodejs里的demo文件夹
cd qcloud-cos-sts-sdk/nodejs/demo/
# 全局安装express
npm install express-generator -g
# 安装所需要的包模块
npm install body-parser request express
可以直接使用 sts-server.js
或者 sts-server-scope.js
修改配置参数,来搭建密钥服务器。
如下修改sts-server.js
里的密钥等配置文件,其中可以看到 demo 使用的是 Express 框架,还需要修改一下服务器运行的端口,防止跟后续的示例冲突,示例:
var bodyParser = require('body-parser');var STS = require('../index');var express = require('express');
// 配置参数var config = { secretId: 'AKID****************', secretKey: '**********', proxy: '', durationSeconds: 1800,
// 放行判断相关参数 bucket: 'buckettest-1250000000', region: 'ap-guangzhou', allowPrefix: '*', // 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径,例子:a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用) // 简单上传和分片,需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923 allowActions: [ // 简单上传 'name/cos:PutObject', 'name/cos:PostObject', // 分片上传 'name/cos:InitiateMultipartUpload', 'name/cos:ListMultipartUploads', 'name/cos:ListParts', 'name/cos:UploadPart', 'name/cos:CompleteMultipartUpload' ],};
// 创建临时密钥服务var app = express();app.use(bodyParser.json());
// 支持跨域访问app.all('*', function (req, res, next) { res.header('Content-Type', 'application/json'); res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'origin,accept,content-type'); if (req.method.toUpperCase() === 'OPTIONS') { res.end(); } else { next(); }});
// 临时密钥接口app.all('/sts', function (req, res, next) {
// TODO 这里根据自己业务需要做好放行判断
// 获取临时密钥 var shortBucketName = config.bucket.substr(0, config.bucket.lastIndexOf('-')); var appId = config.bucket.substr(1 + config.bucket.lastIndexOf('-')); var policy = { 'version': '2.0', 'statement': [{ 'action': config.allowActions, 'effect': 'allow', 'principal': { 'qcs': ['*'] }, 'resource': [ 'qcs::cos:' + config.region + ':uid/' + appId + ':prefix//' + appId + '/' + shortBucketName + '/' + config.allowPrefix, ], }], }; STS.getCredential({ secretId: config.secretId, secretKey: config.secretKey, proxy: config.proxy, durationSeconds: config.durationSeconds, policy: policy, }, function (err, tempKeys) { var result = JSON.stringify(err || tempKeys) || ''; res.send(result); });});app.all('*', function (req, res, next) { res.writeHead(404); res.send({ code: 404, codeDesc: 'PageNotFound', message: '404 page not found' });});
// 启动签名服务app.listen(3333);console.log('app is listening at http://127.0.0.1:3333');
其中/sts
作为导入路由的前缀,是对外提供接口的部分。bucket、region、allowPrefix、allowAction 这几个参数用于设置权限策略 policy 字段,来限定临时密钥所允许访问的资源路径和请求的操作。
由于这里演示的是数据直传,为了测试方便,allowPrefix 这里置为*
,放开整个存储桶的权限,可以根据自己的需要在这里对资源路径进行收缩。允许操作的 Action 默认参数里有 cos:PutObject ,这里可以不做改动。
node sts-server.js# 启动 STS 服务,会看到控制台打印的app is listening at http://127.0.0.1:3333
服务会运行在服务器的 3333 端口,可以在倒数第二行自行修改为其他端口。
在本地浏览器打开 http://ip:port/sts,可以看到云服务器返回的临时密钥信息。
http://42.194.201.209:3333/sts)
二、COS 签名工具
COS 签名工具是腾讯云对象存储为用户提供的 Web 工具,可用于生成请求签名。您可以在工具页面上填入指定的参数,生成请求签名,以及校验请求签名的正确性。
1、基础信息
- API 版本:XML/JSON 版本。
- 签名有效时间:签名的有效时间,默认 60 分钟。可以自定义 Unix 起止时间戳。
2、API 密钥
- API 密钥的参数信息可从控制台的 API 密钥管理页面中获取。
3、HTTP 参数
- HttpMethod:必填项。HTTP 请求方法,包括 GET,POST,PUT,DELETE,HEAD。
- HttpURI:必填项。HTTP 请求 URI 部分,即 Object Key。
- HttpParameters:可选项。HTTP 请求参数。当您需验证 url 参数时可填写该参数。其中,key 小写,value 需要进行 URLEncode,多个 key 以字典排序。
- HttpHeaders:可选项。HTTP 请求头部。当您需验证 url 参数时可填写该参数。其中,key 小写,value 需要进行 URLEncode,多个 key 以字典排序。
点击生成签名后,会看到生成类似以下格式的一种签名串。
q-sign-algorithm=sha1&q-ak=QmFzZTY0IGlzIGEgZ2VuZXJp&q-sign-time=1480932292;1481012292&q-key-time=1480932292;1481012292&q-header-list=host&q-url-param-list=versioning&q-signature=43803ef4a4207299d87bb75d1c739c06cc9406bb
签名串中的各个参数描述如下:
名称 |
描述 |
---|---|
q-sign-algorithm |
描述该签名使用的加密方式,目前腾讯云使用的是 HMAC-SHA1 的方式加密签名。 |
该字段请保持默认值:sha1 |
|
q-ak |
用于标识用户身份 SecretID 的字段 |
q-sign-time |
签名的有效起止时间,其使用 10 位 Unix 时间戳来表示,有效效力精确到秒。 |
该字段通过分号区分起止,起时在前止时在后。 |
|
q-key-time |
可以用户自定义的 SecretKey 有效时间,使用 10 位 Unix 时间戳来表示,有效效力精确到秒。 |
该字段通过分号区分起止,起始时间在前终止时间在后。 |
|
一般 q-key-time 的时间范围大于等于 q-sign-time。 |
|
q-header-list |
提供密文中包含需要校验的 Headers 列表,必须是小写字符。 |
q-url-param-list |
提供密文中包含需要校验的 Parameters 列表,必须是小写字符。 |
q-signature |
经过 HMAC-SHA1 算法加密的请求校验信息。 |
三、验证临时密钥有效性
COS API 使用临时密钥访问 COS 服务时,通过 x-cos-security-token
字段传递临时 sessionToken
,通过临时 SecretId
和 SecretKey
计算签名。
简单来说,就是使用临时密钥里返回的 TmpSecretId
和 TmpSecretKey
去做签名,签名的结果传入 HTTP 请求头部中的 Authorization
字段。
然后把临时密钥返回的 Token 传入 HTTP 请求头部中的 x-cos-security-token
字段。
验证临时密钥的话官网其实也是有 COS 请求工具的,使用起来也比较方便,勾选使用临时密钥,然后分别填入 tmpsecret id 和 key、token 就可以发起请求了。
这里为了更好的理解这里的工作模式,我们选择使用 Postman 工具做一次 PUT 的请求示例。
访问之前已经部署好的临时密钥 STS 服务 URL 地址:
http://42.194.201.209:3333/sts
返回的临时密钥信息如下
{
"expiredTime": 1592792349,
"expiration": "2020-06-22T02:19:09Z",
"credentials": {
"sessionToken": "mfHiMQfhfPUOF67lopyh9KwkCx0mSumV828468f4042e7bd968906ff80cf77ae7bwh6DeY15XgJ6_Ddr9HmT7SOs38bO-nN8ih7izzRM1H2KJDDE46goteDHe2W1qD9YIRY34bIWpFT6HIvRyKKHBGCCZ_SvTvJX_lRnJU5CCksmsuWlxe5qEzJBvzVmn3GrqZ5-5HaOydNKiuhZKo1PphORN-ViyIFOVYSbkWCG3zR3UGig1FomKOBusLpXLkaYa3b9MuasHsmJs71Rv9jE1gLRjpxi3_eRA19sFun6nzkDGD3WeuYgqx_Rf05dOkJAxe2lO6o5t8b5jGUICbdCQThh1b695yw529ffHsR14IHxYMMBuXp_OShDooOBssD-lZquKBhq9LI4MfWELSHu3qiq_-JagSWMIT_CXYmaw6na6U6Tl4syX78XaZCHwoncIOJg-ADiFOxGAF7cEcafPDaiO-Q0dbTuNBQ7Fc2ZlkTTlznhYuj45tlXJ6_hBwrfCY9RRVUQmoBI8CZJ0_C1ExUHUf2p9g4gGZ1fX8yqNY",
"tmpSecretId": "AKIDrXn4jL7XfaZ-Tj-Qt5VYPGjtt637__z57hXYV31EtA4gne4n-RD_D7mspOrMfVI8",
"tmpSecretKey": "ytqC8ZYE7y3ZrUmmKbnZHxB/ZBNc6jEFHMooRisvI8I="
},
"requestId": "fd494be7-0998-462f-9649-baebacaf2f47",
"startTime": 1592790549
}
打开 COS 签名工具,既然演示的是数据直传,那么在这里我们选择以 PUT 的方式传一个文件上去。
签名填的参数如下,SecretId
和 SecretKey
填入刚刚获取到的 tmpSecretId
和 tmpSecretKey
:
打开 Postman,PUT 的请求方式如下,Authorization
字段参数填入刚刚 COS 签名工具生成的签名串,x-cos-security-token
字段参数填入临时密钥返回的 sessionToken
。
点击发送请求,可以看到 COS 服务器返回 200 的状态码,临时密钥验证通过。
四、PUT 直传实践
临时密钥使用的是 Nodejs 的 Express 框架,这里环境为了能跟临时密钥使用的保持一致,也使用 Express 来快速的搭建一个 Web 服务。
1、创建项目
创建一个名为 cos-web-test
的项目,使用 Pug 模板库,不使用 CSS 引擎。
首先,进入准备放置项目的目录,然后在命令提示符运行 Express
应用生成器,生成器将创建(并列出)项目的文件:
[root@VM-0-11-centos data]# mkdir cos-web-test[root@VM-0-11-centos data]# cd cos-web-test/[root@VM-0-11-centos cos-web-test]# express --view=pug
create : public/ create : public/javascripts/ create : public/images/ create : public/stylesheets/ create : public/stylesheets/style.css create : routes/ create : routes/index.js create : routes/users.js create : views/ create : views/error.pug create : views/index.pug create : views/layout.pug create : app.js create : package.json create : bin/ create : bin/www install dependencies: $ npm install
run the app: $ DEBUG=cos-web-test:* npm start
安装依赖包并运行该项目:
npm installnpm start
本地浏览器打开
http://42.194.201.209:3000
可以看到如下效果,代表 Express 应用配置成功。
2、配置 CORS 跨域
进入存储桶详情页,单击【基础配置】页签。下拉页面找到【跨域访问 CORS 设置】配置项,单击【添加规则】,配置示例如下图,详情请参见设置跨域访问文档。
关于跨域的概念和介绍,这里就不具体展开讲了。引申阅读:跨域的基本概念 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
3、页面部署
打开app.js
,在中间添加一行,示例如下,目的为 express.static 中间件函数提供的文件创建虚拟路径前缀 /cos
,为了使用代码在名为 public 的目录中提供的静态资源
app.use('/', indexRouter);app.use('/users', usersRouter);
# 需要添加的行app.use('/cos', express.static('public'));
// catch 404 and forward to error handlerapp.use(function (req, res, next) { next(createError(404));});
打开 Public 目录,新建文件test.html
,示例:
使用 AJAX 上传:AJAX 上传需要浏览器支持基本的 HTML5 特性,当前方案使用 PUT Object 文档,操作指引如下:
- 修改下方代码的 Bucket 和 Region,并复制到 test.html 文件。
- 修改 test.html 里的签名服务地址。
<!doctype html><html lang="en"> <head> <meta charset="UTF-8"> <title>Ajax Put 上传</title> <style> h1, h2 { font-weight: normal; } #msg { margin-top: 10px; }</style></head> <body> <h1>Ajax Put 上传</h1> <input id="fileSelector" type="file"> <input id="submitBtn" type="submit"> <div id="msg"></div> <script src="https://unpkg.com/cos-js-sdk-v5/demo/common/cos-auth.min.js"></script> <script> (function () { // 请求用到的参数 var Bucket = 'shuoweiwu-125****742'; var Region = 'ap-guangzhou'; var protocol = location.protocol === 'https:' ? 'https:' : 'http:'; var prefix = protocol + '//' + Bucket + '.cos.' + Region + '.myqcloud.com/'; // 对更多字符编码的 url encode 格式 var camSafeUrlEncode = function (str) { return encodeURIComponent(str) .replace(/!/g, '%21') .replace(/'/g, '%27') .replace(/(/g, '%28') .replace(/)/g, '%29') .replace(/*/g, '%2A'); }; // 计算签名 var getAuthorization = function (options, callback) { // var url = 'http://127.0.0.1:3000/sts-auth' + // 在这里填入临时密钥STS服务URL地址 var url = 'http://42.194.201.209:3333/sts'; var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function (e) { var credentials; try { credentials = (new Function('return ' + xhr.responseText))().credentials; } catch (e) { } if (credentials) { callback(null, { XCosSecurityToken: credentials.sessionToken, Authorization: CosAuth({ SecretId: credentials.tmpSecretId, SecretKey: credentials.tmpSecretKey, Method: options.Method, Pathname: options.Pathname, }) }); } else { console.error(xhr.responseText); callback('获取签名出错'); } }; xhr.onerror = function (e) { callback('获取签名出错'); }; xhr.send(); }; // 上传文件 var uploadFile = function (file, callback) { var Key = 'dir/' + file.name; // 这里指定上传目录和文件名 getAuthorization({ Method: 'PUT', Pathname: '/' + Key }, function (err, info) { if (err) { alert(err); return; } var auth = info.Authorization; var XCosSecurityToken = info.XCosSecurityToken; var url = prefix + camSafeUrlEncode(Key).replace(/%2F/g, '/'); var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('Authorization', auth); XCosSecurityToken && xhr.setRequestHeader('x-cos-security-token', XCosSecurityToken); xhr.upload.onprogress = function (e) { console.log('上传进度 ' + (Math.round(e.loaded / e.total * 10000) / 100) + '%'); }; xhr.onload = function () { if (/^2dd$/.test('' + xhr.status)) { var ETag = xhr.getResponseHeader('etag'); callback(null, { url: url, ETag: ETag }); } else { callback('文件 ' + Key + ' 上传失败,状态码:' + xhr.status); } }; xhr.onerror = function () { callback('文件 ' + Key + ' 上传失败,请检查是否没配置 CORS 跨域规则'); }; xhr.send(file); }); }; // 监听表单提交 document.getElementById('submitBtn').onclick = function (e) { var file = document.getElementById('fileSelector').files[0]; if (!file) { document.getElementById('msg').innerText = '未选择上传文件'; return; } file && uploadFile(file, function (err, data) { console.log(err || data); document.getElementById('msg').innerText = err ? err : ('上传成功,ETag=' + data.ETag); }); }; })();</script> </body> </html>
打开本地浏览器,输入静态网页地址
http://42.194.201.209:3000/cos/test.html
选择文件,点击上传,上传成功后会在页面上打印出文件的 Etag。
原创声明,本文系作者授权发表,未经许可,不得转载。 如有侵权,请联系小编删除,谢谢。
点击阅读原文,领取 COS 限时1元礼包!
- Pandas Series笔记
- Asp.Net4.0/VS2010新变化(6):内置的图表控件
- Asp.Net4.0/VS2010新变化(5):可扩展的(分布式)缓存
- Pandas对行情数据的预处理
- 上市公司*ST华泽官网打不开,域名已被挂出售卖
- Asp.Net4.0/VS2010新变化(4):SEO的改进
- Pandas DataFrame笔记
- 让控件填满整个页面
- 用多个类别来进行微调
- Asp.Net4.0/VS2010新变化(2):网站自动预热
- Asp.Net4.0/VS2010新变化(1):web.config与publish
- 任天堂将推出Nintendo Labo 域名保护意识墙
- 表格效果
- ROR学习笔记(2):Asp.Net开发者看ROR
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 从Properties乱码来学习编码
- 【Vue.js】Vue.js组件库Element中的图片、回到顶部、无限滚动和抽屉
- Java逐行读取和写入文件
- Vuejs使用v-for指令实现九九乘法表
- Cypress系列(43)- visit() 命令详解
- 在GitLab pages上快速搭建Jekyll博客
- Dubbo项目中No provider available for the service xxx from registry xxx on the consumer问题的解决思路
- Mysql面对高并发修改的问题处理【2】
- java (多网卡环境下)发送组播广播(multicast/broadcast)失败问题
- activmq:android平台下使用openwire协议连接activemq服务的问题
- Leetcode No.9 回文数
- go-zero微服务框架入门教程
- 聊聊java中的哪些Map:(九)TreeMap源码分析
- 海康IPCamera结合OpenCV图像处理的一般步骤
- 聊聊java中的哪些Map:(十)各种map的总结