uniapp阿里云STS上传文件

需求

使用uniapp开发的时候就会遇到有上传图片视频之类的需求,但是我们实际的服务器带宽是有限的,而且如果是先上传到服务器在从服务器上传到OSS也是没有太大意义,所以我们就可以直接从APP里面将文件上传到OSS服务器,然后保存上传后的文件地址即可。

1
用户上传文件 ---> OSS服务器 ---> [得到文件的URL] ---> 后台服务器记录文件信息

问题所在

1. 我们使用uniapp作为APP开发工具,阿里云OSS提供的一些SDK不兼

一开始直接用官方提供node.js的SDK (ali-oss)
处理文件的时候是没有任何问题的,可以直接上传文件,且配置比较简单,只需要后台返回一些配置信息即可,但是后面我打包APP的时候发现出现错误,页面打不开;看日志也只能看到一些粗略的东西,大概意思就是里面使用了dom对象所以没办法初始化。如果是不需要打包app的话是可以直接使用的,可以参考这个文档 上传本地文件

2. 后端需要单独开发一个给前端调用OSS接口获取token的接口

意思就是我们不能直接把我们的accessKeyId 和 accessKeySecret
返回给前台调用,这样不安全。为此OSS也提供了临时访问凭证,配置起来比较麻烦,大家可以参考这个文档 使用STS临时访问凭证访问OSS;跟着文档一步一步来还是可以搞定的。

可能遇到的问题

1. OSS老是提示跨域

这里就正常参照文档提示的跨域配置即可,
参考官方提供的文档 设置跨域规则后调用OSS时仍然报“No ‘Access-Control-Allow-Origin’”的错误 ,
重点来了一定一定使用的上传的地址是你的Bucket域名 而非 Endpoint 这里我是被坑的最惨的时候了,老是提示跨域一直搞一直错。

2. uniapp的代码问题

就是导入库存在的各种问题,这里目前我遇到的就是使用js-base64 这个库的时候一直都是不行的,我也一直都没有搞定,找了别人的一段代码搞定的,不用 js-base64

代码部分

这里就只提供uniapp相关的前端代码,后端代码其实比较简答,主要就是各种阿里云上的配置比较麻烦,有需要可以照着文档一步一步来。

1. 需要引用的依赖

npm install js-base64

具体使用参考 https://www.npmjs.com/package/js-base64

npm install crypto-js

这两个都是用来做加密的,js-base64可以用或者不用

2. 前端代码

上传文件入口程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<view>
<button type="default" @click="selectImages">这是一个大大的按钮</button>
</view>
</template>
<script>

import UploadOss from 'utils/UploadOss'
export default {
methods: {
/**
* 选择上传图片
*/
selectImages() {
uni.chooseImage({
//默认9
count: 1,
//可以指定是原图还是压缩图,默认二者都有
sizeType: ['original', 'compressed'],
//从相册选择
sourceType: ['album'],
success: function (res) {
console.log(res);
UploadOss.uploadFile(res).then(res => {
console.log('上传文件的结果为:', res)
})
}
});
}
}
}
</script>


base64.js

这里主要是因为我使用 js-base64 库会出问题, 如果你的不会则不需要此段代码,可以直接使用js-base64的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 私钥
const _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
// 加密
const encode = function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = _utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
return output;
}
// 解密
const decode = (input) => {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
if (input == undefined || input == null) {

} else {
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = _keyStr.indexOf(input.charAt(i++));
enc2 = _keyStr.indexOf(input.charAt(i++));
enc3 = _keyStr.indexOf(input.charAt(i++));
enc4 = _keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = _utf8_decode(output);
return output;
}

}
// private method for UTF-8 encoding
const _utf8_encode = (string) => {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
// private method for UTF-8 decoding
const _utf8_decode = (utftext) => {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
var c1 = 0;
var c2 = 0;
var c3 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if ((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}

module.exports = {
decode: decode,
encode: encode
};

上传文件JS文件

具体业务逻辑JS中有描述,这里不做赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
const crypto = require('crypto-js')
// 这里主要是因为我使用 js-base64 库会出问题, 如果你的不会则不需要此段代码,可以直接使用js-base64的库
// const base64 = require('js-base64')
const base64 = require('./base64s')
// 获取STS参数信息的接口地址
const tokenurl = 'https://baicu.com/gettoken';
// 本地缓存数据的key
const cacheKey = "xxxxtoken";
// 本地数据有效期
const validtimes = 1000 * 100;

/**
* 上传文件
* @param fileobj
* @returns {Promise<unknown>}
*/
export function uploadFile(fileobj) {
const filename = fileobj.tempFiles[0].name;
const filepath = fileobj.tempFilePaths[0];
return new Promise((resolve, reject) => {
getFormData(filename).then(data => {
uni.uploadFile({
//后台返回的阿里云存储的上传地址
url: data.host,
filePath: filepath,
fileType: "image",
name: 'file',
formData: data.formdata,
success: res => {
uni.hideLoading();
uni.showToast({title: '上传成功', icon: 'success', duration: 2000});
console.log(data.host + data.formdata.key);
resolve(data.host + data.formdata.key);
},
fail: err => {
reject(err)
uni.hideLoading();
uni.showModal({
content: err.errMsg,
showCancel: false
});
}
});
})
})
}

/**
* 获取上传文件需要的formdata数据
*/
const getFormData = (filename) => {
// 先从本地获取token
return new Promise((resolve => {
getToken().then(res => {
// 获取到的token为
console.log('获取到的token为', res)
const ossAccessKeyId = res.accessKeyId;
const securityToken = res.stsToken;
// 这里遇到过坑, 上传文件的路径一定是bucket域名
const aliyunServerURL = 'https://' + res.bucket + '.' + res.endpoint;
const policy = getPolicyBase64(5, 150)
const signature = computeSignature(res.accessKeySecret, policy);
const aliyunFileKey = getOssPath(filename)

const formdata = {
//文件名
key: aliyunFileKey,
//后台获取超时时间
policy: policy,
//后台获取临时ID
OSSAccessKeyId: ossAccessKeyId,
// 如果是STS的话这里的参数必须要
'x-oss-security-token': securityToken,
//让服务端返回200,不然,默认会返回204
success_action_status: '200',
//后台获取签名
signature: signature
};
let data = {
host: aliyunServerURL,
formdata: formdata
}
resolve(data);
})
}))
}

/**
* 获取token信息的方法
*/
const getToken = () => {
return new Promise((resolve, reject) => {
// 先从缓存中获取数据
const tempData = uni.getStorageSync(cacheKey);
// 是否有效
let isvalid = false;
if (tempData && tempData !== '') {
// 数据存在,判断数据是否过期
const tokenObj = JSON.parse(tempData);
// 判断是否过期 (createtime:是后端指点的创建时间字段)
var diff = new Date().getTime() - Number.parseFloat(tokenObj.createtime);
// 判断数据是否过期
isvalid = diff < validtimes;
}

if (isvalid) {
// 数据有效且未过期则使用缓存中的数据
resolve(JSON.parse(tempData));
} else {
// 无有效数据或数据已过期则从新获取数据
getToken4Api(cacheKey).then(res => {
resolve(res);
})
}
})
}


/**
* 从网关获取数据
* @param cacheKey
*/
const getToken4Api = (cacheKey) => {
return new Promise((resolve, reject) => {
uni.request({
url: tokenurl,
success: (res) => {
if (res.data.status === 200) {
// 将数据存到本地存储, 由于token信息是有有效时间的,
uni.setStorageSync(cacheKey, JSON.stringify(res.data.data))
resolve(res.data.data)
}
},
fail: (err => {
reject(err);
})
})
})
}

/**
* 设置OSS的存储路径
* @param filename 文件名称
* @returns {string}
*/
const getOssPath = (filename) => {
const current_timestamp = new Date().getTime()
const upload_name = crypto.MD5(current_timestamp + Math.random() * 10000) + filename;
const current_day = dateformat('YYYYmmdd', new Date())
return 'files/' + current_day + '/' + upload_name
}

/**
* 日期格式化方法
* @param fmt ex: yyyy-MM-dd HH:mm:ss
* @param date new Date()
* @returns {*}
*/
const dateformat = (fmt, date) => {
let ret;
const opt = {
"Y+": date.getFullYear().toString(), // 年
"m+": (date.getMonth() + 1).toString(), // 月
"d+": date.getDate().toString(), // 日
"H+": date.getHours().toString(), // 时
"M+": date.getMinutes().toString(), // 分
"S+": date.getSeconds().toString() // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (let k in opt) {
ret = new RegExp("(" + k + ")").exec(fmt);
if (ret) {
fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
}
}
return fmt;
}

/**
* 安全策略需要转成base64
* @param timeout 超时时间
* @param maxsize 文件大小限制
* @returns {*}
*/
const getPolicyBase64 = function (timeout, maxsize) {
const date = new Date()
// 限制参数的生效时间(单位:小时)。
const timeOut = timeout || 1
// 限制上传文件大小(单位:MB)。 1MB = 1048576B
const maxSize = maxsize || 10
date.setHours(date.getHours() + timeOut)
const policyText = {
expiration: date.toISOString(),
conditions: [
['content-length-range', 0, maxSize * 1024 * 1024]
]
}
// policy必须为base64的string。
return base64.encode(JSON.stringify(policyText))
}

/**
* 计算签名
* @param accessKeySecret
* @param canonicalString
* @returns {string}
*/
const computeSignature = function (accessKeySecret, canonicalString) {
return crypto.enc.Base64.stringify(
crypto.HmacSHA1(canonicalString, accessKeySecret)
)
}


module.exports = {
uploadFile: uploadFile
};


尾巴

uniapp上传文件到oss真的是搞死我了,一开始使用ali-oss我感觉so easy,但是打包才发现特别坑,查阅了网上很多的教程,大家描述的都挺模糊或者和我搞得模式不太一样,所以特意记录一下这个问题,希望能帮助有着和我一样问题的人。

鸣谢

小程序文件OSS直传阿里云

阿里云对象上传服务器签名