阿里云对象存储服务(Object Storage Service,简称OSS),是阿里云对外提供的海量、安全、低成本、高可靠的云存储服务。
目前通过Web端直传文件(Object)到OSS,有两种方案:
一、利用OSS Browser.js SDK将文件上传到OSS。该方案通过OSS Browser.js SDK直传数据到OSS,支持断点续传,支持各种主流浏览器,可以将File对象、Blob数据以及OSS Buffer上传OSS,该方案还支持下载和删除
二、利用OSS提供的PostObject接口来实现表单上传,不支持断点续传,支持h5,小程序,支持uniapp的uni.uploadFile接口
方案一:使用阿里云SDK上传
由于前端环境不安全,为避免暴露阿里云账号访问密钥(AccessKey ID和AccessKey Secret),该方案需要搭建STS服务获取临时访问密钥(AccessKey ID和AccessKey Secret)和安全令牌(SecurityToken),需要先开通STS服务,参考官方文档
后端
后端需要导入aliyun-sdk-oss包,用于获取前端需要的key和secret
JDK版本:jdk8
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.0.7</version>
<!--<version>3.15.0</version>最新-->
</dependency>
如果是java9及以上版本,则需要添加jaxb相关依赖。添加jaxb相关依赖示例代码如下:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
搭建STS服务(部分代码)
// STS接入地址,例如sts.cn-hangzhou.aliyuncs.com。
@Value("${ramEndpoint}")
private String ramEndpoint;
// 访问密钥AccessKey ID和AccessKey Secret
@Value("${ramAccessKeyId}")
private String ramAccessKeyId;
@Value("${ramAccessKeySecret}")
private String ramAccessKeySecret;
// 角色ARN
@Value("${ramRoleArn}")
// 自定义角色会话名称,用来区分不同的令牌,例如可填写为SessionTest
private String ramRoleArn;
@Value("${ramRoleSessionName}")
private String ramRoleSessionName;
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
RedisTemplate redisTemplate;
/**
* 通过RAM子账号获取stsToken,作为临时凭据
*/
@RequestMapping(value = "getStsToken", method = {RequestMethod.GET, RequestMethod.POST})
public Object getStsToken() {
ResponseVo responseVo = new ResponseVo();
AssumeRoleResponse response = null;
Object redisToken = null;
try {
redisToken = redisTemplate.opsForValue().get("stsToken");
} catch (Exception e) {
logger.error(e.getMessage());
responseVo.setError(GlobalErrorCode.SYS_RUN_ERROR.getCode());
return responseVo;
}
if (redisToken != null) {
response = JSONObject.parseObject(redisToken.toString(), AssumeRoleResponse.class);
responseVo.setSuccess(response);
return responseVo;
} else {
String policy = "{\n" +
" \"Version\": \"1\", \n" +
" \"Statement\": [\n" +
" {\n" +
" \"Action\": [\n" +
" \"oss:*\"\n" +
" ], \n" +
" \"Resource\": [\n" +
" \"acs:oss:*:*:*\" \n" +
" ], \n" +
" \"Effect\": \"Allow\"\n" +
" }\n" +
" ]\n" +
"}";
try {
DefaultProfile.addEndpoint("", "", "Sts", ramEndpoint);
// 构造default profile(参数留空,无需添加region ID)
IClientProfile profile = DefaultProfile.getProfile("", ramAccessKeyId, ramAccessKeySecret);
// 用profile构造client
DefaultAcsClient client = new DefaultAcsClient(profile);
final AssumeRoleRequest request = new AssumeRoleRequest();
request.setMethod(MethodType.POST);
request.setRoleArn(ramRoleArn);
request.setRoleSessionName(ramRoleSessionName);
request.setPolicy(policy); // 若policy为空,则用户将获得该角色下所有权限
request.setDurationSeconds(20 * 60L); // 设置凭证有效时间,单位秒
//获取凭证
response = client.getAcsResponse(request);
/*
* 缓存该凭证,凭证失效后才从OSS再次获取凭证
* 凭证有效时间为20分钟,Redis里只缓存10分钟
*/
redisTemplate.opsForValue().set("stsToken", JSONObject.toJSONString(response), 10 * 60, TimeUnit.SECONDS);
responseVo.setSuccess(response);
return responseVo;
} catch (ClientException e) {
logger.error(e.getErrMsg());
responseVo.setError(GlobalErrorCode.SYS_RUN_ERROR.getCode());
return responseVo;
}
}
}
参考阿里云文档
前端
安装
$ npm install ali-oss --save
部分代码
onLoad() {
this.getStsToken()
},
methods
/**
* @param {String} pathAndName Object完整路径。Object完整路径中不能包含Bucket名称("exampledir/exampleobject.txt")
* @param {Object} data (file对象、Blob数据或者OSS Buffer)
*/
async putObject(pathAndName, data) {
try {
// 您可以通过自定义文件名(例如exampleobject.txt)或文件完整路径(例如exampledir/exampleobject.txt)的形式实现将数据上传到当前Bucket或Bucket中的指定目录。
const result = await this.getClient().put(
pathAndName,
data
);
console.log('result:', result);
} catch (e) {
console.log(e);
}
},
getClient() {
if (this.client) {
return this.client
}
const OSS = require('ali-oss');
const client = new OSS({
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: 'oss-cn-qingdao',
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
accessKeyId: this.stsToken.credentials.accessKeyId,
accessKeySecret: this.stsToken.credentials.accessKeySecret,
// 从STS服务获取的安全令牌(SecurityToken)。
stsToken: this.stsToken.credentials.securityToken,
refreshSTSToken: async () => {
// 向您搭建的STS服务获取临时访问凭证。
let info = await this.$post(GET_STS_TOKEN)
info = info.data
console.log('-----------refresh--token')
return {
accessKeyId: info.credentials.accessKeyId,
accessKeySecret: info.credentials.accessKeySecret,
stsToken: info.credentials.securityToken
}
},
// 刷新临时访问凭证的时间间隔,单位为毫秒。每隔一段时间定时器会自动掉后台接口刷新token
refreshSTSTokenInterval: 600000,
// 填写Bucket名称。
bucket: 'zxxxxth-bucket'
});
this.client = client
return this.client
},
getStsToken() {
//从后台获取stsToken(改成自己的前端请求接口)
this.$post(GET_STS_TOKEN).then(rsp => {
if (rsp.success) {
this.stsToken = rsp.data;
// 初始化一下client让定时任务启动,自动刷新token(避免过期)
this.getClient()
console.log('this.stsToken:', this.stsToken)
} else {
uni.showToast({
title: rsp.message,
duration: 2000
});
}
})
},
refreshSTSToken参数说明:当初始化new OSS()时,定时器会启动,当时间到了refreshSTSTokenInterval所设置的值时,并不会立即调用后台接口获取token,只有手动触发put()接口时,才会调用后台接口获取token
参考阿里云文档
开通STS服务步骤
方案二:使用PostObject接口来实现表单上传
这个方案支持小程序上传,uniapp上传。无需开通STS服务
后端
获取postObject接口需要的policy,OSSAccessKeyId,signature 参考官方文档
这里签名使用后端签名,所以不需要申请开通STS服务
简化版,无回调
/**
* 利用OSS提供的PostObject接口,通过表单上传的方式将文件上传到OSS。
* 该方案兼容大部分浏览器,但在网络状况不好的时候,如果单个文件上传失败,
* 只能重试上传。上传的Object大小不能超过5 GB。
* @return ResponseVo{success:true,message:'',data:{},code:200}
*/
@RequestMapping(value = "getPostObjectParams", method = {RequestMethod.GET, RequestMethod.POST})
public Object getPostObjectParams() {
ResponseVo responseVo = new ResponseVo();
responseVo.setSuccess(OSSServer.getPostObjectParams());
return responseVo;
}
OSSServer.class
public static OSSClient getOSSClient() {
if (null == ossClient) {
ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
}
return ossClient;
}
/**
* 获取表单上传的方式的PostObject参数
* @return
*/
public static Map<String, Object> getPostObjectParams() {
Map<String, Object> respMap = new LinkedHashMap();
// 限制参数的生效时间,单位为分钟,默认值为20。
int expireTime = 20;
// 限制上传文件的大小,单位为MB,默认值为100。
int maxSize = 100;
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
// 如果值为"test"那么前端的key参数必须以"test"开头,如test/*、test1.jpg、test/comment/11.jpg
String dir = "";
// 创建OSSClient实例。
OSS ossClient = getOSSClient();
try {
long expireEndTime = System.currentTimeMillis() + expireTime * 1000 * 60;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize * 1024 * 1024);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap.put("accessKeyId", accessKeyId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("expire", expireEndTime / 1000);
} catch (Exception e) {
log.error("getPostObjectParams", e);
}
return respMap;
}
大多数情况下,用户上传文件后,应用服务器需要知道用户上传了哪些文件以及文件名;如果上传了图片,还需要知道图片的大小等,为此OSS提供了上传回调方案。
流程图:
当用户要上传一个文件到OSS,而且希望将上传的结果返回给应用服务器时,需要设置一个回调函数,将请求告知应用服务器。用户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户。
有回调的PostObject参数
/**
* 获取表单上传的方式的PostObject参数【有回调】
* @return
*/
public static Map<String, Object> getPostObjectParams() {
Map<String, Object> respMap = new LinkedHashMap();
// 限制参数的生效时间,单位为分钟,默认值为20。
int expireTime = 20;
// 限制上传文件的大小,单位为MB,默认值为10。
int maxSize = 10;
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
// 如果值为"test"那么前端的key参数必须以"test"开头,如test/*、test1.jpg、test/comment/11.jpg
// 可以让用户没有办法上传到其他的目录,从而保证了数据的安全性
String dir = "";
// 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
String callbackUrl = "https://jmt.xxx.cn/common/postObjectCallBack/";
// 创建OSSClient实例。
OSS ossClient = getOSSClient();
try {
long expireEndTime = System.currentTimeMillis() + expireTime * 1000 * 60;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize * 1024 * 1024);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap.put("accessKeyId", accessKeyId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("expire", expireEndTime / 1000);
// 配置回调地址
JSONObject jasonCallback = new JSONObject();
jasonCallback.put("callbackUrl", callbackUrl);
// jasonCallback.put("callbackBody",
// "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
jasonCallback.put("callbackBody", "{\"filename\":${object},\"mimeType\":${mimeType}}");
jasonCallback.put("callbackBodyType", "application/json");//application/x-www-form-urlencoded
String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
respMap.put("callback", base64CallbackBody);
} catch (Exception e) {
log.error("getPostObjectParams", e);
}
return respMap;
}
给oss回调的接口
@RequestMapping(value = "postObjectCallBack", method = RequestMethod.POST)
public Object postObjectCallBack(HttpServletRequest request, @RequestBody Object callbackBody) throws IOException {
log.info("---callbackBody={}",callbackBody);
// "{"filename":"test/comment/tt1.jpg","mimeType":"image/png"}"
return callbackBody;
}
前端
小程序
const host = '<host>'; //"https://examplebucket.oss-cn-hangzhou.aliyuncs.com"
const signature = '<signatureString>';
const ossAccessKeyId = '<accessKey>';
const policy = '<policyBase64Str>';
const key = '<object name>';
const securityToken = '<x-oss-security-token>';
const filePath = '<filePath>'; // 待上传文件的文件路径。
wx.uploadFile({
url: host, // 这个是阿里云bucket的根地址,使用这个地址拼接路径可以访问已上传的文件。如果是自己服务器则是开发者服务器的URL。
filePath: filePath,// 本地文件路径,小程序chooseImage方法返回的路径
name: 'file', // 必须填file。
formData: {
key,
policy,
OSSAccessKeyId: ossAccessKeyId,
signature,
// 'x-oss-security-token': securityToken // 使用STS签名时必传。
},
success: (res) => {
if (res.statusCode === 204) {
console.log('上传成功');
}
},
fail: err => {
console.log(err);
}
});
uniapp、uView的Upload组件
uni.uploadFile({
url: 'https://res.xxx.cn', //这个是阿里云bucket的根地址,使用这个地址拼接路径可以访问已上传的文件
filePath: url,// 本地文件路径,小程序chooseImage方法返回的路径
name: 'file',// 必须填file
formData: {
key: 'test/comment/tt1.jpg',//会把tt1.jpg图片上传至bucket(上方url所指向)的test/comment目录
policy: this.postObject.policy,
OSSAccessKeyId: this.postObject.accessKeyId,
signature: this.postObject.signature,
// callback: this.postObject.callback
},
success: (res) => {
console.log('uni.uploadFile success:', res)
if(res.statusCode===204){
// 上传成功
console.log('-------------success------------')
}else if(res.statusCode===403){
// Policy expired.
uni.showToast({
title: '网络超时',
duration: 2000
});
// 续期
this.getPostObjectParams()
}else{
console.log('上传失败')
}
// setTimeout(() => {
resolve(res)
// }, 1000)
},
fail(err) {
console.error('uni.uploadFile: fail', err)
}
});
onLoad() {
this.getPostObjectParams()
},
...........
getPostObjectParams() {
//从后台获取stsToken
this.$post(GET_POST_OBJECT_PARAMS).then(rsp => {
if (rsp.success) {
this.postObject = rsp.data;
console.log('this.postObject:', this.postObject)
} else {
uni.showToast({
title: rsp.message,
duration: 2000,
icon:'none'
});
}
})
},
参考官方文档
举一个uniapp例子
UI库:uView
<template>
<!-- 发表评论 -->
<view class="create-comment">
<view class="star comment-common">
<view class="title">评分</view>
<view class="control">
<text class="name">游玩体验</text>
<u-rate :count="5" v-model="starCount" :touchable="false" active-color="#E65526" size="24"></u-rate>
</view>
</view>
<view class="content comment-common">
<view class="title">评价内容</view>
<textarea v-model="resourceComment.content" maxlength="200"
placeholder="游玩的满意吗?大家都想了解这里值得去吗?有什么亮点?期待你精彩的点评!">
</textarea>
</view>
<view class="picture comment-common">
<view class="title">图片</view>
<!-- name=1对应fileList1 -->
<u-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="15"
:maxSize="maxSize">
</u-upload>
</view>
<view class="picture comment-common">
<view class="title">视频</view>
<!-- name=2对应fileList2 -->
<u-upload :fileList="fileList2" @afterRead="afterRead" @delete="deletePic" name="2" multiple :maxCount="1"
:maxSize="maxSize" accept="video" uploadIcon="movie"></u-upload>
</view>
<button @click="submit()" type="warn" class="submit" :loading="loading" :disabled="loading">发布</button>
</view>
</template>
<script>
import {
GET_POST_OBJECT_PARAMS
} from '../../api/api.js'
export default {
data() {
return {
starCount: 0,
resourceComment: {
content: ''
},
fileList1: [],
fileList2: [],
loading: false,
postObject: {
expire: 0
},
maxSize: 100 * 1024 * 1024
}
},
onLoad() {
},
methods: {
// -----upload start
// 新增图片
async afterRead(event) {
console.log('event:', event)
// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
let lists = [].concat(event.file)
// console.log('lists:', lists)
let fileListLen = this[`fileList${event.name}`].length
// console.log('fileListLen:', fileListLen)
lists.map((item) => {
this[`fileList${event.name}`].push({
...item,
status: 'uploading',
message: '上传中'
})
})
let time = new Date().getTime() / 1000
// console.log('time:', time)
if (time > this.postObject.expire) {
// policy过期,续期
await this.getPostObjectParams()
}
for (let i = 0; i < lists.length; i++) {
const result = await this.uploadFilePromise(lists[i].url)
let item = this[`fileList${event.name}`][fileListLen]
this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
status: result ? 'success' : 'failed',
message: '',
url: result
}))
fileListLen++
}
},
compressJpgImage(src) {
return new Promise((resolve, reject) => {
// uni.compressImage({
// src: src,
// quality: 80,
// success: res => {
// console.log(res.tempFilePath)
// }
// })
})
},
uploadFilePromise(url) {
return new Promise((resolve, reject) => {
let a = uni.uploadFile({
url: 'https://res.xxxx.cn',
filePath: url,
name: 'file', // 必须填file
formData: {
key: 'test/comment/tt3.jpg',
policy: this.postObject.policy,
OSSAccessKeyId: this.postObject.accessKeyId,
signature: this.postObject.signature,
callback: this.postObject.callback
},
success: (res) => {
// 未配置回调 上传成功返回 {date:"",errMsg:"uploadFile:ok",statusCode:204},如果配置了回调data参数才会有值
// 配置了回调 上传成功返回 {{data:{"filename":"test/comment/tt1.jpg","mimeType":"image/png"},errMsg:"uploadFile:ok",statusCode:200}
console.log('uni.uploadFile success():', res)
if (res.statusCode === 204 || res.statusCode === 200) {
// 上传成功
console.log('-------------uploaded success')
resolve(url)
} else {
console.log('-------------uploaded failed')
uni.showToast({
title: '上传失败',
duration: 2000,
icon: 'error'
});
resolve()
}
},
fail(err) {
console.error('uni.uploadFile: fail():', err)
}
});
})
},
// 删除图片
deletePic(event) {
this[`fileList${event.name}`].splice(event.index, 1)
},
// -----upload end
getPostObjectParams() {
//从后台获取postObject
return this.$post(GET_POST_OBJECT_PARAMS,{folderType:'comment'}).then(rsp => {
// this.postObject = rsp
if (rsp.success) {
this.postObject = rsp.data;
console.log('this.postObject:', this.postObject)
} else {
console.error('getPostObjectParams:', rsp.message || '系统错误')
}
})
}
}
}
</script>
<style lang="scss">
.create-comment {
padding: 12px;
.comment-common {
margin-bottom: 10px;
padding: 15px 10px;
background-color: white;
border-radius: 10px;
}
.title {
margin-bottom: 10px;
font-size: 16px;
font-weight: bold;
}
.star {
.control {
display: flex;
align-items: center;
.name {
margin-right: 10px;
}
}
}
.content {
textarea {
font-size: 14px;
width: 100%;
}
}
.picture {}
.submit {
margin-top: 20px;
width: 80%;
font-size: 15px;
color: white;
background-color: #e65526;
}
}
</style>
附:根据blob链接获取blob对象
/**
* 根据blob链接获取blob对象
* @param {Object} url "blob:http://localhost:8085/d688ce4f-0f5d-418c-85ad-62bcb3f38dee"
* @returns Blob(31846) {size: 31846, type: "image/jpeg"}
*/
getBlobByUrl(url) {
return uni.request({
url: url,
// 合法值:text、arraybuffer
responseType: 'arraybuffer'
}).then(data=>{
const [error, rsp] = data;
if(error){
console.error(`post-error:${error}, url:${url}`)
return {message: error.errMsg}
}else{
let buffer = rsp.data
// ArrayBuffer(185) {}
console.log('buffer:', buffer)
return new Blob([buffer])
}
})
// return new Promise((resolve, reject) => {
// let xhr = new XMLHttpRequest()
// xhr.open('GET', url, true)
// xhr.responseType = 'blob'
// xhr.onload = function(e) {
// if (this.status == 200) {
// let myBlob = this.response
// // let file = new window.File(
// // [myBlob],
// // 'myfile.jpg', {
// // type: myBlob.type
// // }
// // )
// // console.log("files:", file)
// resolve(myBlob)
// } else {
// reject(false)
// }
// }
// xhr.send()
// })
},
blobUrl、blob、base64、file相互转化:https://www.cnblogs.com/jing-zhe/p/15402775.html
uniapp选择file
<button @click="submit()" type="warn" class="submit" :loading="loading" :disabled="loading">发布</button>
submit() {
let utils = new Utils()
uni.chooseImage({
count: 6, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album'], //从相册选择
success: function(res) {
console.log(JSON.stringify(res.tempFilePaths));
console.log(res.tempFiles)
utils.getFileMD5(res.tempFiles[0], function(md5) {
console.log('md5:', md5)
})
}
});
},