提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
练手的项目中使用到了OSS的签名直传,将该过程进行记录,以便后续使用。前端采用的是vue-admin-template框架的上传组件,后端采用java完成
Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到OSS。这种方式需通过应用服务器中转,传输效率明显低于数据直传至OSS的方式。数据直传至OSS是利用OSS的PostObject接口,使用表单上传方式上传文件至OSS
2.1 签名直传无需将Accesskey暴露在前端页面,相比JavaScript客户端签名直传具有更高的安全性
2.2 签名直传无需将资源传到应用服务器再转发到OSS中,而是直接上传至OSS中,节省了带宽
后端的代码较为简单,只需返回直传所需的一些验证参数即可,具体参数可参照官方文档:阿里云OSS直传.
Controller层:
@Api("oss管理接口")
@RestController
@RequestMapping("/ossService")
public class OssController
{
private final OssService ossService;
public OssController(OssService ossService) {
this.ossService = ossService;
}
@ApiOperation(value = "获取oss签名")
@GetMapping("/getOssSign")
public R getOssSign(){
Map<String, Object> signMap = ossService.getOssSign();
if(signMap == null || signMap.size() == 0){
return R.error().msg("未获取到签名,请重试");
}
return R.ok().data(signMap);
}
}
Service层:
@Service
public class OssServiceImpl implements OssService
{
@Override
public Map<String, Object> getOssSign() {
String accessId = ConstantPropertiesUtils.KEY_ID; // 请填写您的AccessKeyId。
String accessKey = ConstantPropertiesUtils.KEY_SECRET; // 请填写您的AccessKeySecret。
String endpoint = ConstantPropertiesUtils.END_POINT; // 请填写您的 endpoint。
String bucket = ConstantPropertiesUtils.BUCKET; // 请填写您的 bucketname 。
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String dir = new DateTime().toString("yyyy/MM/dd"); // 用户上传文件时指定的前缀。
Map<String, Object> respMap = null;
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
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 = new LinkedHashMap<>();
//这些参数名必须要这样写,与官方文档一一对应
respMap.put("OSSAccessKeyId", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("Signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
//让服务端返回200,不然,默认会返回204
//respMap.put("success_action_status", 200);
}
catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
finally {
ossClient.shutdown();
}
return respMap;
}
}
前端使用的是vue-admin-template框架,其中文件上传用的是ImageCropper和Hamburger组件,由于对前端代码不熟,因此碰到了一些问题(有坑,需要自己改部分源码)
<el-form-item label="讲师头像">
<pan-thumb :image="String(teacher.avatar)" />
<!-- 文件上传按钮 -->
<el-button type="primary" icon="el-icon-upload" @click="beforeUpload"
>更换头像
</el-button>
<image-cropper
v-show="imagecropperShow"
:width="300"
:height="300"
:key="imagecropperKey"
:url="this.ossObj.host"
field="file"
:params="this.ossObj"
@close="closeUpload"
@crop-upload-success="uploadSuccess"
/>
</el-form-item>
其中有2个主要的函数,其作用分别为:
前端js代码
//获取oss的token和签名
beforeUpload() {
this.imagecropperShow = true;
//获取后台签名
oss.getOssSign().then((response) => {
console.log(response);
this.ossObj = response.data;
this.ossObj.key = response.data.dir + "/" + this.randomString(10);
console.log(this.ossObj.key);
});
},
//上传成功的回调
uploadSuccess(data) {
this.imagecropperShow = false;
//上传之后拼接url,这样后续提交表单数据时便可保存url到数据库中
let url = this.ossObj.host + "/" + this.ossObj.key;
this.teacher.avatar = url;
this.imagecropperKey = this.imagecropperKey + 1;
},
//用于随机生成文件名
randomString(len) {
len = len || 32;
var chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
var maxPos = chars.length;
var pwd = "";
for (var i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
},
解决:查看ImageCropper的源码发现,所有的其余参数都应该加到:parms中,这样会自动上传
The bucket POST must contain the specified ‘key‘. If it is specified, please check the order
报错原因:阿里云OSS规定文件上传时,必须携带key参数,且key参数必须在上传的file之前,如果key在file后或者缺少key参数,便会报错。而ImageCropper源码在填充参数时是先将file放在第一位,后面的参数依次遍历添加,因此出现了key在file后面的情况,导致报错
// 807行
fmData.append(
field,
data2blob(createImgUrl, mime),
field + "." + imgFormat
);
//添加其他参数
if (typeof params === "object" && params) {
Object.keys(params).forEach((k) => {
fmData.append(k, params[k]);
});
}
解决:将代码顺序进行修改,这样key就会在file之前,报错解决
if (typeof params === "object" && params) {
Object.keys(params).forEach((k) => {
fmData.append(k, params[k]);
});
}
fmData.append(
field,
data2blob(createImgUrl, mime),
field + "." + imgFormat
);
报错原因:vue-admin-template封装了axios请求,加入了拦截器,因此它对于是否上传成功的判断是,如果后端返回的data.code===20000则成功,否则失败。但是OSS是外部服务器,不会出现20000的响应,因此应当根据响应状态码进行判断
解决:在封装axios请求的js代码中新增判断条件(修改response拦截器即可),不仅code为20000时可以成功,并且响应码为204时也可以成功
utils/request.js:
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
response => {
/**
* code为非20000是抛错 可结合自己业务进行修改
*/
const res = response.data
if (response.status === 204 || res.code === 20000) {
return response.data
} else {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm(
'你已被登出,可以取消继续留在该页面,或者重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
阿里云OSS签名直传是如今比较流行的文件上传方式,本文介绍了签名直传的实现,后端代码实现较为简单,主要出的坑都在前端(主要是前端框架的使用问题),应当加强前端代码能力
TO DO
签名直传还有回调功能,该回调功能应当写在后端代码中,但实现起来与签名直传并无太大区别,加上回调函数即可,具体可参照服务端签名直传并设置上传回调