任何问题都有解决的办法,无法可想的事是没有的。──爱迪生
问题场景:
一个thymeleaf
项目,上传图片、视频是直接使用Java
代码保存到服务器上
于是服务器压力激增,网页静态资源变得卡顿
于是
我们需要将图片、视频上传移植到阿里云OSS
上
阿里云对象存储OSS(Object Storage Service)是阿里云提供的海量、安全、低成本、高持久的云存储服务。
一句话描述:它是一个很棒的存取文件的在线服务器
我们可以使用它进行客户端上传,文件不通过服务端也可以上传,这样减轻了我们服务端压力
我们需要一系列配置才可以对接完成,查阅阿里云OSS
官方文档发现:JavaScript客户端签名直传的方式最为简单,但这种方式是有弊端的,客户端通过JavaScript
把存取文件的认证信息写在代码里面有泄露的风险
因此
我们采用服务端签名后直传的方式进行上传文件,我们待会儿介绍~
准备工作:
登录阿里云官网。
将鼠标移至产品,单击对象存储 OSS,打开 OSS 产品详情页面。
在 OSS 产品详情页,单击立即开通。
开通服务后,在 OSS 产品详情页单击管理控制台直接进入 OSS 管理控制台界面。
您也可以单击位于官网首页右上方菜单栏的控制台,进入阿里云管理控制台首页,然后单击左侧的对象存储 OSS 菜单进入 OSS 管理控制台界面。
根据官方文档提供步骤视频可以完成开通、创建Bucket
、进行授权、配置跨域等
我们到阿里云RAM
访问控制台来添加一个用户
填入用户账号信息、勾选编程访问后点击确定
然后我们点击复制,找个地方粘贴下来我们的AccessKey ID
以及AccessKey Secret
然后我们点击添加权限为该用户授权
开始对接
官方文档流程图片:
这里可以这么理解:
假设小王接到老板的任务:将这堆货物放到仓库中去,但仓库大门的门禁需要门卡
所以上图中的类比
1:小王去保管室拿门卡
2:小王拿到门卡回来
3:小王搬运货物到仓库,存放货物
因此我们首先先做“保管室”的功能
后端代码实现
这里就是用我们的Java
代码在服务端实现:
引入alicloud-oss
的依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> <version>2.2.0.RELEASE</version> </dependency>
application.yml
,注意版本或者依赖不同,配置有可能改变,例如我最下面的Demo
使用的boot
的依赖(配置方式发生了变化,不过我们代码都还是一样的,只是注意@Value需要写对)
spring: cloud: alicloud: access-key: <你的AccessKey> secret-key: <你的SecretKey> oss: endpoint: <你的endpoint(可以在你的OSS控制台概览看到)> bucket: <你的bucket(可以在你的OSS控制台概览看到)>
Controller
package com.ruben.simpleoss.controller; import com.ruben.simpleoss.service.OssService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Map; /** * 上传Controller * * @author <achao1441470436@gmail.com> * @since 2021/6/18 17:40 */ @RestController public class UploadController { @Resource private OssService ossService; /** * oss上传文件 * * @author <achao1441470436@gmail.com> * @since 2021/6/17 10:07 */ @GetMapping("oss") public Map<String, Object> oss() { return ossService.getMark(); } }
Service
package com.ruben.simpleoss.service; import java.util.Map; /** * Oss服务层 * * @author <achao1441470436@gmail.com> * @since 2021/6/17 10:21 */ public interface OssService { Map<String, Object> getMark(); }
ServiceImpl
package com.ruben.simpleoss.service.impl; import com.aliyun.oss.OSS; import com.aliyun.oss.common.utils.BinaryUtil; import com.aliyun.oss.model.MatchMode; import com.aliyun.oss.model.PolicyConditions; import com.ruben.simpleoss.service.OssService; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * @ClassName: UploadServiceImpl * @Description: 获取OSS签证 * @Date: 2020/6/3 21:55 * * * @author: achao<achao1441470436 @ gmail.com> * @version: 1.0 * @since: JDK 1.8 */ @Service public class OssServiceImpl implements OssService { @Resource OSS ossClient; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.oss.bucket}") private String bucket; @Value("${spring.cloud.alicloud.access-key}") private String accessId; /** * 获取临时签证 * * @return */ @Override public Map<String, Object> getMark() { String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。 // String callbackUrl = "http://88.88.88.88:8888"; String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String dir = format + "/"; // 用户上传文件时指定的前缀。 Map<String, String> respMap = null; try { long expireTime = 3000; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); 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(StandardCharsets.UTF_8); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); respMap = new LinkedHashMap<String, String>(8); respMap.put("accessid", accessId); respMap.put("policy", encodedPolicy); respMap.put("signature", postSignature); respMap.put("dir", dir); respMap.put("host", host); respMap.put("expire", String.valueOf(expireEndTime / 1000)); // respMap.put("expire", formatISO8601Date(expiration)); } catch (Exception e) { // Assert.fail(e.getMessage()); System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } Map<String, Object> result = new HashMap<>(4); result.put("data", respMap); result.put("code", 20000); return result; } }
然后我们运行访问一下,就像第一步小王去拿门卡了
可以看到拿到我们的临时凭证了,就像第二步小王拿着门卡回来了
然后我们现在要拿着这些凭证去上传文件,就像第三步小王要去存货物
按照PostObject
的文档测试一下
上传完成之后我们这里是没有回调的,因为我们这里没有配置callback
,如有需要,可以查看Callback
文档
上传完成之后我们可以在控制台看到我们上传的文件
然后如果我们在代码里使用文件的url
的话,就使用host
+/
+(key
替换掉${filename}
占位符为文件名)即可
例如我的host
是https://will-deprecated.oss-cn-chengdu.aliyuncs.com
加上/
再加上key
为:2021-06-18/17585793-95b9-4dbd-8dfc-2302d6afa5df_${filename}
替换掉文件名就是2021-06-18/17585793-95b9-4dbd-8dfc-2302d6afa5df_partnerhead.jpg
(其实我测试过了,不用${filename}
占位符,直接使用文件名也可以上传成功)
最终https://will-deprecated.oss-cn-chengdu.aliyuncs.com/2021-06-18/17585793-95b9-4dbd-8dfc-2302d6afa5df_partnerhead.jpg
则是我们完整的文件路径
前端代码实现
使用html
+vue
主页面,引用组件的地方
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>OSS上传Demo</title> <link href="/css/elementUI.css" rel="stylesheet"> <script src="/js/jquery-3.5.1.min.js"></script> <script src="/js/vue.min.js"></script> <script src="/js/elementUI.js"></script> <script src="/js/singleUpload.js"></script> </head> <body> <div id="myapp"> <single-upload ref="singleUpload" v-model="imageUrl"></single-upload> 文件url:{ {imageUrl} } </div> <script> new Vue({ el: '#myapp', data: { imageUrl: '' } }) </script> </body> </html>
自己写的组件:
function policy() { return new Promise((resolve, reject) => { $.ajax({ url: '/oss', type: 'GET', contentType: 'application/json; charset=UTF-8', success: function (res) { if (res.code == 20000) { resolve(res) } else { reject(res) } }, error: function (res) { reject(res) } }); }); } Vue.component('singleUpload', { props: ['value'], template: ' <div class="single-upload">' + ' <el-upload' + ' :before-upload="beforeUpload"' + ' :data="dataObj"' + ' :file-list="fileList"' + ' :multiple="false"' + ' :on-preview="handlePreview"' + ' :on-remove="handleRemove"' + ' :on-success="handleUploadSuccess"' + ' :show-file-list="showFileList"' + ' :action="dataObj.host"' + ' list-type="text"' + ' style="display: flex;"' + ' >' + ' <el-button size="small" type="primary">点击上传</el-button>' + ' </el-upload>' + ' <el-dialog :modal="false" :visible.sync="dialogVisible">' + ' <img width="100%;" v-if="isImg(fileList[0].url)" :src="fileList[0].url" alt="图片找不到了..."/>' + ' <video width="900px" controls autoplay muted v-if="isVideo(fileList[0].url)" :src="fileList[0].url" alt="视频找不到了..."/>' + ' </el-dialog>' + ' </div>', data() { return { dataObj: { policy: "", signature: "", key: "", ossaccessKeyId: "", dir: "", host: "" // callback:'', }, dialogVisible: false }; }, computed: { imageUrl() { return this.value; }, imageName() { if (this.value != null && this.value !== "") { return this.value.substr(this.value.lastIndexOf("/") + 1); } else { return null; } }, fileList() { return [ { name: this.imageName, url: this.imageUrl } ]; }, showFileList: { get: function () { return ( this.value !== null && this.value !== "" && this.value !== undefined ); }, set: function (newValue) { } } }, methods: { isVideo() { let fileType = this.getFileType() return ~['.mp4', '.avi'].indexOf(fileType) }, isImg() { let fileType = this.getFileType() return ~['.png', '.jpg', '.jpeg', '.gif'].indexOf(fileType) }, getFileType() { let fileType = this.value.substring(this.value.lastIndexOf('.')) return fileType }, getUUID() { //生成UUID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16) }) }, emitInput(val) { this.$emit("input", val); }, handleRemove(file, fileList) { this.emitInput(""); }, handlePreview(file) { this.dialogVisible = true; }, beforeUpload(file) { let _self = this; return new Promise((resolve, reject) => { policy() .then(response => { console.log(response) _self.dataObj.policy = response.data.policy; _self.dataObj.signature = response.data.signature; _self.dataObj.ossaccessKeyId = response.data.accessid; _self.dataObj.key = response.data.dir + this.getUUID() + "_${filename}"; _self.dataObj.dir = response.data.dir; _self.dataObj.host = response.data.host; resolve(true); }) .catch(err => { reject(false); }); }); }, handleUploadSuccess(res, file) { console.log("上传成功..."); this.showFileList = true; this.fileList.pop(); this.fileList.push({ name: file.name, url: this.dataObj.host + "/" + this.dataObj.key.replace("${filename}", file.name) }); this.emitInput(this.fileList[0].url); console.log(this.fileList[0]); } } })
注意引入一些文件:
最终效果:
项目源码
本次案例完整项目代码:https://gitee.com/VampireAchao/simple-oss.git
这个项目基本能应对绝大部分场景,它那次请求签证接口是使用的jQuery
的ajax
如果您是纯vue
项目,可以看我以往这个项目https://gitee.com/VampireAchao/my-vue-app.git
小结
通过对这次的OSS
对接,复习了如何在html
页面中引入vue
、elementUI
、引入vue
自定义组件、进行第三方服务接入等…
最后,希望本文能对您有所帮助~