From 494308aaf05dfd058a1c5bb004a5242b24dbb6de Mon Sep 17 00:00:00 2001 From: weiyie <912881342@qq.com> Date: Thu, 9 Jan 2020 14:36:08 +0800 Subject: [PATCH] feat: support uploads directly with signature --- .eslintrc.js | 5 + README.md | 17 ++ example/formPost.md | 42 ++++ example/package.json | 4 +- example/server/postObject.js | 112 +++++++++ example/src/template/postObject.html | 265 ++++++++++++++++++++ lib/common/object/calculatePostSignature.js | 28 +++ lib/common/object/index.js | 3 +- lib/common/utils/policy2Str.js | 17 ++ test/node/object.test.js | 47 ++++ 10 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 example/formPost.md create mode 100644 example/server/postObject.js create mode 100644 example/src/template/postObject.html create mode 100644 lib/common/object/calculatePostSignature.js create mode 100644 lib/common/utils/policy2Str.js diff --git a/.eslintrc.js b/.eslintrc.js index 6484f28e2..74aba2fd5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,11 @@ /* eslint max-len: [0] */ module.exports = { extends: 'airbnb', + parserOptions: { + ecmaFeatures: { + experimentalObjectRestSpread: true, + }, + }, env: { browser: true, node: true, diff --git a/README.md b/README.md index 40dfb411c..9b34d94d5 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ All operation use es7 async/await to implement. All api is async function. - [.listParts(name, uploadId[, query, options])](#listparts-name-uploadid-query-options) - [.listUploads(query[, options])](#listuploadsquery-options) - [.abortMultipartUpload(name, uploadId[, options])](#abortmultipartuploadname-uploadid-options) + - [.calculatePostSignature(policy)](#calculatePostSignaturepolicy) - [RTMP Operations](#rtmp-operations) - [.putChannel(id, conf[, options])](#putchannelid-conf-options) - [.getChannel(id[, options])](#getchannelid-options) @@ -2550,6 +2551,22 @@ const result = await store.abortMultipartUpload('object', 'upload-id'); console.log(result); ``` +### .calculatePostSignature(policy) + +get postObject params + +parameters: + +- policy {JSON or Object} policy must contain expiration and conditions. + +Success will return postObject Api params. + +Object: + +- OSSAccessKeyId {String} +- Signature {String} +- policy {Object} response info + ## RTMP Operations All operations function is [async], except `getRtmpUrl`. diff --git a/example/formPost.md b/example/formPost.md new file mode 100644 index 000000000..b8aad7a45 --- /dev/null +++ b/example/formPost.md @@ -0,0 +1,42 @@ +本文以JavaScript语言为例,讲解在服务端通过JavaScript代码完成签名,客户端通过请求获取签名,然后通过表单直传数据到OSS。 + +### 前提条件 +应用服务器对应的域名可通过公网访问。 +确保应用服务器已经安装node 8.*以上版本 +确保PC端浏览器支持JavaScript。 + +### 步骤1:配置应用服务器 +1. 下载应用服务器源码 +2. 打开example/server/postObject.js文件 +修改对应的`accessKeyId`、`accessKeySecret`、`bucket`配置 +``` + const config = { + accessKeyId: '', // + accessKeySecret: '', // + bucket: '' + } +``` +如果使用通过STS临时授权方式进行表单直传, 则需要修改对应`STS_ROLE`配置 +``` + const STS_ROLE = ''; +``` +3. 执行`npm install` +4. 执行`node postObject.js` + +### 步骤2:修改CORS +客户端进行表单直传到OSS时,会从浏览器向OSS发送带有`Origin`的请求消息。OSS对带有`Origin`头的请求消息会进行跨域规则(CORS)的验证。因此需要为Bucket设置跨域规则以支持Post方法。 + +1. 登录[OSS管理控制台](https://oss.console.aliyun.com/?spm=a2c4g.11186623.2.16.548e4c07WTCBqs)。 +2. 在左侧存储空间列表中,单击目标存储空间名称,打开该存储空间概览页面。 +3. 单击**基础设置**页签,找到**跨域设置**区域,然后单击**设置**。 +4. 单击**创建规则**,配置如下图所示。 +![](http://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/assets/img/9610949651/p12308.png) + +### 步骤3:体验表单上传 +1. 在浏览器输入`http://localhost:9000/` + +2. 填写上传后的文件名称 + +3. 选择上传的文件 + +4. 单击**上传**按钮 \ No newline at end of file diff --git a/example/package.json b/example/package.json index d93fbf749..c56dd1df4 100644 --- a/example/package.json +++ b/example/package.json @@ -7,7 +7,8 @@ "server": "node server/app.js", "watch": "webpack -w --config ./config/webpack.dev.conf.js", "dev": "webpack-dev-server --config ./config/webpack.dev.conf.js", - "start": "node ./script.js" + "start": "node ./script.js", + "server-post": "node server/postObject.js" }, "author": "", "license": "MIT", @@ -30,6 +31,7 @@ "dependencies": { "ali-oss": "^6.0.0", "bootstrap": "^4.1.1", + "express": "^4.17.1", "jquery": "^3.3.1" } } diff --git a/example/server/postObject.js b/example/server/postObject.js new file mode 100644 index 000000000..b44a71f9d --- /dev/null +++ b/example/server/postObject.js @@ -0,0 +1,112 @@ +const express = require('express'); +const OSS = require('../../'); +const { STS } = require('../../'); + +const app = express(); +const path = require('path'); + +const config = { + accessKeyId: 'accessKeyId', + accessKeySecret: 'accessKeySecret', + bucket: 'bucket' +}; + +const STS_ROLE = 'STS_ROLE'; + +app.get('/postObject', async (req, res) => { + const client = new OSS(config); + + const date = new Date(); + date.setDate(date.getDate() + 1); + const policy = { + expiration: date.toISOString(), // 请求有效期 + conditions: [ + ['content-length-range', 0, 1048576000] // 设置上传文件的大小限制 + // { bucket: client.options.bucket } // 限制可上传的bucket + ] + }; + + const formData = await client.calculatePostSignature(policy); + const url = `http://${config.bucket}.${ + (await client.getBucketLocation()).location + }.aliyuncs.com`; + const params = { + formData, + url + }; + + res.json(params); +}); +app.get('/postObjectBySTS', async (req, res) => { + if (STS_ROLE === 'STS_ROLE') { + res.status(500); + res.json({ + message: '请修改 STS_ROLE ' + }); + } + const stsClient = new STS(config); + + const date = new Date(); + date.setDate(date.getDate() + 1); + const STSpolicy = { + Statement: [ + { + Action: ['oss:*'], + Effect: 'Allow', + Resource: ['acs:oss:*:*:*'] + } + ], + Version: '1' + }; + + const formData = {}; + try { + const result = await stsClient.assumeRole( + STS_ROLE, + STSpolicy, + 3600 // token过期时间 单位秒 + ); + const { credentials } = result; + + const client = new OSS({ + accessKeyId: credentials.AccessKeyId, + accessKeySecret: credentials.AccessKeySecret, + bucket: config.bucket, + stsToken: credentials.SecurityToken + }); + + const policy = { + expiration: date.toISOString(), // 请求有效期 + conditions: [ + ['content-length-range', 0, 1048576000] // 设置上传文件的大小限制 + // { bucket: client.options.bucket } // 限制可上传的bucket + ] + }; + + const signatureFormData = await client.calculatePostSignature(policy); + Object.assign(formData, signatureFormData); + formData['x-oss-security-token'] = credentials.SecurityToken; + const url = `http://${config.bucket}.${ + (await client.getBucketLocation()).location + }.aliyuncs.com`; + const params = { + formData, + url + }; + + res.json(params); + } catch (error) { + res.status(500); + res.json(error); + } +}); + +app.use('/static', express.static('public')); +app.get('/', (req, res) => { + res.sendFile(path.resolve(__dirname, '../src/template/postObject.html')); +}); + +app.listen(9000, () => { + console.log('http://localhost:9000'); + console.log('App of postObject started.'); +}); diff --git a/example/src/template/postObject.html b/example/src/template/postObject.html new file mode 100644 index 000000000..f41312d8c --- /dev/null +++ b/example/src/template/postObject.html @@ -0,0 +1,265 @@ + + + + + + + JavaScript postObject + + + + + + + +
+
+
+
+ 1. postObject +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ 2. postObject By STS +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ + + diff --git a/lib/common/object/calculatePostSignature.js b/lib/common/object/calculatePostSignature.js new file mode 100644 index 000000000..b6b1ec87b --- /dev/null +++ b/lib/common/object/calculatePostSignature.js @@ -0,0 +1,28 @@ + +const policy2Str = require('../utils/policy2Str'); +const signHelper = require('../signUtils'); + +const proto = exports; + +/** + * @param {Object or JSON} policy specifies the validity of the fields in the request. + * @return {Object} params + * {String} params.OSSAccessKeyId + * {String} params.Signature + * {String} params.policy JSON text encoded with UTF-8 and Base64. + */ +proto.calculatePostSignature = function calculatePostSignature(policy) { + if (!policy) { + throw new Error('policy must be JSON or Object'); + } + policy = Buffer.from(policy2Str(policy), 'utf8').toString('base64'); + + const Signature = signHelper.computeSignature(this.options.accessKeySecret, policy); + + const query = { + OSSAccessKeyId: this.options.accessKeyId, + Signature, + policy + }; + return query; +}; diff --git a/lib/common/object/index.js b/lib/common/object/index.js index 412b8176f..802d22354 100644 --- a/lib/common/object/index.js +++ b/lib/common/object/index.js @@ -5,5 +5,6 @@ const proto = exports; merge(proto, require('./getSymlink')); merge(proto, require('./putSymlink')); merge(proto, require('./getObjectMeta')); -merge(proto, require('./copyObject.js')); +merge(proto, require('./copyObject')); +merge(proto, require('./calculatePostSignature')); diff --git a/lib/common/utils/policy2Str.js b/lib/common/utils/policy2Str.js new file mode 100644 index 000000000..7b559b489 --- /dev/null +++ b/lib/common/utils/policy2Str.js @@ -0,0 +1,17 @@ +function policy2Str(policy) { + let policyStr; + if (policy) { + if (typeof policy === 'string') { + try { + policyStr = JSON.stringify(JSON.parse(policy)); + } catch (err) { + throw new Error(`Policy string is not a valid JSON: ${err.message}`); + } + } else { + policyStr = JSON.stringify(policy); + } + } + return policyStr; +} + +module.exports = policy2Str; diff --git a/test/node/object.test.js b/test/node/object.test.js index 8d7ced057..0871ddf75 100644 --- a/test/node/object.test.js +++ b/test/node/object.test.js @@ -1767,4 +1767,51 @@ describe('test/object.test.js', () => { // console.log(result); }); }); + + + describe('calculatePostSignature()', () => { + it('should get signature for postObject', async () => { + const name = 'calculatePostSignature.js'; + const url = store.generateObjectUrl(name).replace(name, ''); + const date = new Date(); + date.setDate(date.getDate() + 1); + const policy = { + expiration: date.toISOString(), + conditions: [ + { bucket: store.options.bucket } + ] + }; + + const params = store.calculatePostSignature(policy); + + const options = { + url, + method: 'POST', + formData: { + ...params, + key: name, + file: { + value: 'calculatePostSignature', + options: { + filename: name, + contentType: 'application/x-javascript' + } + } + } + }; + + const postFile = () => + new Promise((resolve, reject) => { + request(options, (err, res) => { + if (err) reject(err); + if (res) resolve(res); + }); + }); + + const result = await postFile(); + assert(result.statusCode === 204); + const headRes = await store.head(name); + assert.equal(headRes.status, 200); + }); + }); });