Spring Boot整合Minio实现文件上传

Spring Boot整合Minio后,前端的文件上传有两种方式:

  1. 文件上传到后端,由后端保存到Minio

  2. 这种方式好处是完全由后端集中管理,可以很好的做到、身份验证、权限控制、文件与处理等,并且可以做一些额外的业务逻辑,比如生成缩略图、提取元数据等。

  3. 缺点也很明显:

  4. 延迟时间高了,本来花费上传一次文件的时间,现在多了后端保存到Minio的时间

  5. 后端资源占用,后端本来可以只处理业务请求,现在还要负责文件流,增加了性能压力

  6. 单点故障,Minio即便做了集群,但是如果后端服务器故障,也会导致Minio不可用

  7. 所以,实际上我们不会把文件传到后端,而是直接传给Minio,其实这也符合OSS服务的使用方式。

  8. 文件向后端申请上传凭证,然后直接上传到Minio

  9. 为了避免Minio被攻击,我们需要结合后端,让后端生成并返回一个有时效的上传凭证,前端拿着这个凭证才能去上传,通过这种方式,我们可以做到一定程度的权限控制,本文要分享的就是这种方式。

环境准备

  • 部署好的Minio环境:http://mylocalhost:9001

Spring Boot整合Minio

简单过一下整合方式把。

先引入Minio依赖

# pom.xml 
<dependency> <groupId>io.minio</group Id> 
<artifactId>minio</artifact Id> 
<version>7.1.0</version> 
</dependency>

然后定义配置信息

# application.yml
minio:
endpoint: http://mylocalhost:9001 
accessKey: minio 
secretKey: minio123 
bucket: demo

定义一个属性类

@Component 
@ConfigurationProperties(prefix = "minio")
public class MinioProperties { /** * 对象存储服务的URL */ private String endpoint; /** * Access key就像用户ID,可以唯一标识你的账户 */ private String accessKey;/** * Secret key是你账户的密码*/private String secretKey; /*** 默认文件桶*/ private String bucket; ...
}

定义Minio配置类

@Configuration 
public class MinioConfig { @Beanpublic MinioClient minioClient(MinioProperties properties){ try { MinioClient.Builder builder = MinioClient.builder(); builder.endpoint(properties.getEndpoint());if (StringUtils.hasLength(properties.getAccessKey()) && StringUtils.hasLength(properties.getSecretKey())) { builder.credentials(properties.getAccessKey(),properties.getSecretKey()); } return builder.build(); } catch (Exception e){ return null;}}
}

现在启动服务即可。

上传凭证

写一个接口,返回上传凭证:

@RequestMapping(value = "/presign", method = {RequestMethod.POST}) 
public Map<String, String> presign(@RequestBody PresignParam presignParam) {//如果前端不指定桶,那么给一个默认的if (StringUtils.isEmpty(presignParam.getBucket())) {presignParam.setBucket("demo"); } // 前端不指定文件名称,就给一个UUID if (StringUtils.isEmpty(presignParam.getFilename())) { presignParam.setFilename(UUID.randomUUID().toString());} // 如果想要以子目录的方式保存,就在前面加上斜杠来表示 //      presignParam.setFilename("/2023/" + presignParam.getFilename());// 设置凭证过期时间 ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(10); // 创建一个凭证 PostPolicy policy = new PostPolicy(presignParam.getBucket(), presignParam.getFilename(), expirationDate); // 限制文件大小,单位是字节byte,也就是说可以设置如:只允许10M以内的文件上传 // policy.setContentRange(1, 10 * 1024); // 限制上传文件请求的ContentType // policy.setContentType("image/png");try {// 生成凭证并返回final Map<String, String> map = minioClient.presignedPostPolicy(policy);for (Map.Entry<String, String> entry : map.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); } return map; } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) { e.printStackTrace();}return null;
}

上面的示例代码可以知道,我们还可以加一些权限认证,以判断用户是否有以下权限:

  • 上传权限

  • 可上传的文件大小

  • 可上传的文件类型

请求参数类:

public class PresignParam { // 桶名private String bucket; // 文件名 private String filename; ...
}

这个接口的返回结果是:

bucket: demo 
x-amz-date: 20230831T042351Z 
x-amz-signature: 79cc2ae0baee274d1d47cb29bdd5e99127059033503c2a02f904f0478a73ecac
key: 寂寞的季节.mp4 
x-amz-algorithm: AWS4-HMAC-SHA256
x-amz-credential: minio/20230831/us-east-1/s3/aws4_request 
policy: eyJleHBpcmF0aW9uIjoiMjAyMy0wOC0zMVQwNDozMzo1MS42MzZaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIkYnVja2V0IiwiZGVtbyJdLFsiZXEiLCIka2V5Iiwi5a+C5a+e55qE5a2j6IqCLm1wNCJdLFsiZXEiLCIkeC1hbXotYWxnb3JpdGhtIiwiQVdTNC1ITUFDLVNIQTI1NiJdLFsiZXEiLCIkeC1hbXotY3JlZGVudGlhbCIsIm1pbmlvLzIwMjMwODMxL3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QiXSxbImVxIiwiJHgtYW16LWRhdGUiLCIyMDIzMDgzMVQwNDIzNTFaIl1dfQ==
  • bucket:表示目标桶

  • x-amz-date:时间戳

  • x-amz-signature:签名

  • key:文件名

  • x-amz-algorithm:签名算法

  • x-amz-credential:认证授权

  • policy:凭证token

前端收到后,将该凭证连同文件流一并上传到Minio服务器:

uploadFile(file, policy) { console.log("准备上传文件:") console.log("file:" + file) console.log("policy:" + policy) var formData = new FormData() formData.append('file', file) formData.append('key', policy['key']) formData.append('x-amz-algorithm', policy['x-amz-algorithm'])formData.append('x-amz-credential', policy['x-amz-credential']) formData.append('x-amz-signature', policy['x-amz-signature']) formData.append('x-amz-date', policy['x-amz-date']) formData.append('policy', policy['policy'])return new Promise(((resolve, reject) => {$.ajax({method: 'POST',url: 'http://mylocalhost:9001/' + policy['bucket'],data: formData, dataType: 'json', contentType: false, // 必须设置为 false,不设置 contentType,让浏览器自动设置processData: false, // 必须设置为 false,不对 FormData 进行序列化处理 // async: false, // 设置同步,方便等下做分片上传xhr: function xhr() {//获取原生的xhr对象 var xhr = $.ajaxSettings.xhr(); if (xhr.upload) { //添加 progress 事件监听 xhr.upload.addEventListener('progress', function (e) {//e.loaded 已上传文件字节数 //e.total 文件总字节数var percentage = parseInt(e.loaded / e.total * 100) vm.uploadResult = percentage + "%" + ":" + policy['key']}, false); } return xhr; }, success: function (result) { vm.uploadResult = '文件上传成功:' + policy['key'] resolve(result) }, error: function (e) { reject()} }) }))
}

这样就完成了获取上传凭证并上传文件。

分片上传、秒传、断点续传

分片上传

分片上传可以用在大文件上传上,一个100M的文件可以分成10份,每份10M,一共传输10次,这有以下好处:

  • Minio做了集群,用Nginx转发,那么分片上传可以降低单台Minio服务器的性能压力

  • 多线程上传可以加快上传效率

秒传

现在说说秒传,我们上传一个文件之前,可以用工具生成MD5字符串,就好像这样:

3cc1f3c3c2d1a29ecf60ffad4de278c7

然后拼接上文件名:

3cc1f3c3c2d1a29ecf60ffad4de278c7_寂寞的季节.mp4

这时候去向后端申请上传凭证的时候,后端可以先去看看文件是否已存在,如果文件已存在,就不用生成凭证了,直接告诉前端该文件已经上传完毕,由此实现文件秒传。
这样的好处是:

  • 降低Minio服务器压力

  • 响应秒回,用户体验提高

断点续传

结合分片上传和秒传的原理,我们可以来做到断点续传。

场景:当我们要上传一个大文件的时候,进度到一半了,这时候网络掉线导致上传失败,网络恢复后又要重新上传,这就很崩溃。

处理方式:大文件也可以分成一个个小文件来上传,这样即便上传到一半网络掉线,恢复上传的时候可以跳过前一半已上传的部分,接着上传后面一半。

文件合并

当我们分片上传后,后端还需要提供接口,来将所有分片数据合并:

@GetMapping("/compose")public void merge() {List<ComposeSource> sources = new ArrayList<>();// 分片数据放到另一个桶里面:slice sources.add(ComposeSource.builder() .bucket("slice") .object("0寂寞的季节.mp4").build()); sources.add(ComposeSource.builder().bucket("slice") .object("1寂寞的季节.mp4").build());sources.add(ComposeSource.builder().bucket("slice").object("2寂寞的季节.mp4") .build());final ComposeObjectArgs args = ComposeObjectArgs.builder() .bucket("demo") .object("寂寞的季节.mp4").sources(sources).build(); try { minioClient.composeObject(args);} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {e.printStackTrace(); } }

上面的示例很简单,因为只做演示说明。
前端需要传的参数是:

  • 分片桶:slice

  • 分片数据数组: 0寂寞的季节.mp4 1寂寞的季节.mp4 2寂寞的季节.mp4

  • 目标桶:demo

然后调用composeObject函数完成合并。

前端示例代码分享

上面就是关于实战经验分享的全部了,因为需要前端配置来使用,所以这里给出我这篇文章的前端示例,很简单的单页面(技术栈就别吐槽了):

<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"><title>Title</title> <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script> 
</head> 
<body> 
<div id="app"> <h1>{{title}}</h1>
<br> <form @submit.prevent="getPolicyForm"><label> 桶名 <input type="text" v-model="policyParams.bucket"></label>
<br><label> 文件名 <input type="text" v-model="policyParams.filename"> </label> 
<br> <button type="submit">获取上传凭证</button>
<br> <div v-for="(val, key) in policy" :key="key">{{ key }}: <span>{{ val }}</span></div></form> 
<br> <form @submit.prevent="uploadFileForm" v-show="policy != null"><label> 文件 <input type="file" @change="fileChange"></label> 
<br> <br><button type="submit" v-show="file != null">上传文件</button>
</form> --- 
<br> <div v-show="file != null"> <button @click="sliceEvent">测试文件分片上传</button> | <button @click="sliceComposeEvent">分片文件合并</button> </div>
<br><br> <br> <p>{{uploadResult}}</p> 
<ul> <!-- <li v-for="item in sliceUploadResult">{{ item }}</li>--> <li v-for="(item, index) in sliceUploadResult" :key="index">{{ item }}</li> </ul> <br> </div> <script> var vm = new Vue({el: "#app", data() { return { title: "Minio测试" // 请求凭证参数 , policyParams: { bucket: null , filename: null } // 请求到的凭证 , policy: null // 待上传文件 , file: null// 上传文件参数 , uploadParams: { file: null } // 分片上传参数 , sliceParams: { bucket: "" , filename: "" , file: null } , slicePolicys: [] , sliceCount: 0// 上传结果回调 , uploadResult: null // 分片上传结果回调 , sliceUploadResult: null };}, methods: { getPolicyForm() {this.policyParams.bucket = "demo" this.policyParams.filename = "寂寞的季节.mp4" this.requestPolicy(this.policyParams)}, requestPolicy(params) { return new Promise(((resolve, reject) => { $.ajax({ type: "POST", url: "http://localhost:8888/presign", contentType: "application/json", data: JSON.stringify(params),// async: false, success: function (result) { console.log(result) vm.policy = result; resolve(result) }, error: function (e) { reject() }});}))}, fileChange(event) { const file = event.target.files[0] this.file = file }, uploadFileForm() { this.uploadFile(this.file, this.policy) }, uploadFile(file, policy) { console.log("准备上传文件:") console.log("file:" + file) console.log("policy:" + policy) var formData = new FormData() formData.append('file', file) formData.append('key', policy['key']) formData.append('x-amz-algorithm', policy['x-amz-algorithm']) formData.append('x-amz-credential', policy['x-amz-credential']) formData.append('x-amz-signature', policy['x-amz-signature'])formData.append('x-amz-date', policy['x-amz-date']) formData.append('policy', policy['policy']) return newPromise(((resolve, reject) => { $.ajax({ method: 'POST', url: 'http://mylocalhost:9001/' + policy['bucket'], data: formData, dataType: 'json', contentType: false, // 必须设置为 false,不设置 contentType,让浏览器自动设置 processData: false,// 必须设置为 false,不对 FormData 进行序列化处理 // async: false, // 设置同步,方便等下做分片上传 xhr: function xhr() { //获取原生的xhr对象 var xhr = $.ajaxSettings.xhr(); if (xhr.upload) { //添加 progress 事件监听 xhr.upload.addEventListener('progress', function (e) {//e.loaded 已上传文件字节数 //e.total 文件总字节数 var percentage = parseInt(e.loaded / e.total * 100) vm.uploadResult = percentage + "%" + ":" + policy['key'] }, false);} return xhr; }, success: function (result) { vm.uploadResult = '文件上传成功:' + policy['key'] resolve(result) }, error: function (e) { reject() } })}))
}, sliceEvent() { // 获取文件 var file = this.file// 设置分片大小:5MB var chunkSize = 5 * 1024 * 1024 // 计算总共有多少个分片 var totalChunk = Math.ceil(file.size / chunkSize)// 数组存放所有分片 var chunks = [] // 遍历所有分片 for (var i = 0; i < totalChunk; i++) { // 利用slice获取分片 var start = i * chunkSize var end = Math.min(file.size, start + chunkSize) var blob = file.slice(start, end)// 添加分片到数组 chunks.push(blob) } console.log(totalChunk) this.sliceUploadResult = Array(totalChunk).fill(0) for (let i = 0; i < chunks.length; i++) { var file = chunks[i]; this.calculateMD5(file) .then((md5) => { console.log(md5);// 输出计算出的 MD5 值 }) .catch((error) => { console.error(error); // 处理错误 }); } return// 创建序号 var index = 0; // 循环上传分片 while (index < totalChunk) { console.log('------------------------------') params = { "bucket": "slice", "filename": index + "寂寞的季节.mp4" } var policyPromise = this.requestPolicy(params); (function (index) { var file = chunks[index] policyPromise.then(function (result) { var filename = result['key'] console.log('准备上传文件:', filename, ',序号为:', index) vm.uploadFile(file, result).then(function (result) { console.log('上传完成:' + filename) vm.sliceUploadResult[index] = ('分片文件上传成功:' + filename) }) }) })(index) index++ } }, sliceComposeEvent() { var parmas = {} $.ajax({ method: 'POST', url: 'http://localhost:8888/compose', data: formData, dataType: 'json', contentType: false, // 必须设置为 false,不设置 contentType,让浏览器自动设置 processData: false, // 必须设置为 false,不对 FormData 进行序列化处理 // async: false, // 设置同步,方便等下做分片上传 xhr: function xhr() { //获取原生的xhr对象 var xhr = $.ajaxSettings.xhr(); if (xhr.upload) { //添加 progress 事件监听 xhr.upload.addEventListener('progress', function (e) { //e.loaded 已上传文件字节数 //e.total 文件总字节数 var percentage = parseInt(e.loaded / e.total * 100) vm.uploadResult = percentage + "%" + ":" + policy['key'] }, false);} return xhr; },success: function (result) { vm.uploadResult = '文件上传成功:' + policy['key'] resolve(result) },error: function (e) { reject() }
}) 
}, calculateMD5(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); // 读取文件内容 reader.readAsArrayBuffer(file); reader.onload = () => { const spark = new SparkMD5.ArrayBuffer(); spark.append(reader.result); // 将文件内容添加到 MD5 计算器中 const md5 = spark.end();// 计算 MD5 值 resolve(md5); }; reader.onerror = (error) => { reject(error); }; });}
}, mounted() { 
}, created() {
}, 
});
</script> 
</body> 
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/66452.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

金融租赁系统助力行业转型与升级的创新之路

内容概要 在当今快速发展的商业环境中&#xff0c;金融租赁系统逐渐成为企业转型与升级的重要工具。它通过整合大数据与自动化技术&#xff0c;不仅提升了风险管理的准确性&#xff0c;还加快了审批流程&#xff0c;让企业在激烈的市场竞争中游刃有余。这个系统就像是一位聪明…

Postman接口测试02|接口用例设计

目录 六、接口用例设计 1、接口测试的测试点&#xff08;测试维度&#xff09; 1️⃣功能测试 2️⃣性能测试 3️⃣安全测试 2、设计方法与思路 3、单接口测试用例 4、业务场景测试用例 1️⃣分析测试点 2️⃣添加员工 3️⃣查询员工、修改员工 4️⃣删除员工、查询…

Python标准库之SQLite3

包含了连接数据库、处理数据、控制数据、自定义输出格式及处理异常的各种方法。 官方文档&#xff1a;sqlite3 --- SQLite 数据库的 DB-API 2.0 接口 — Python 3.13.1 文档 官方文档SQLite对应版本&#xff1a;3.13.1 SQLite主页&#xff1a;SQLite Home Page SQL语法教程&a…

HTML 迷宫游戏

HTML 迷宫游戏 相关资源文件已经打包成压缩文件&#xff0c;可双击index.html直接运行程序&#xff0c;且文章末尾已附上相关源码&#xff0c;以供大家学习交流&#xff0c;博主主页还有更多Python相关程序案例&#xff0c;秉着开源精神的想法&#xff0c;望大家喜欢&#xff0…

【Linux】上传、下载、压缩、解压

一、上传、下载 1.1 FinalShell文件系统 我们可以通过FinalShell工具&#xff0c;方便的和虚拟机进行数据交换。 在FinalShell软件的下方窗体中&#xff0c;提供了Linux的文件系统视图&#xff0c;可以方便的&#xff1a; 浏览文件系统&#xff0c;找到合适的文件&#xff0…

以柔资讯-D-Security终端文件保护系统 logFileName 任意文件读取漏洞复现

0x01 产品简介 D-Security终端文件保护系统是一套专注于企业文件管理效率与安全的解决方案,统对文件进行全文加密,而非仅在文件表头或特定部分进行加密,从而大大提高了文件的安全性,降低了被破解的风险。D-Security终端文件保护系统是被政府和国安局等情报单位唯一认定的安…

关于重构一点简单想法

关于重构一点简单想法 当前工作的组内&#xff0c;由于业务开启的时间正好处于集团php-》go技术栈全面迁移的时间点&#xff0c;组内语言技术栈存在&#xff1a;php、go两套。 因此需求开发过程中通常要考虑两套技术栈的逻辑&#xff0c;一些基础的逻辑也没有办法复用。 在这…

新的 WhoisXML API 白皮书重点分析了主要 gTLD 和 ccTLD 注册趋势

任何寻求建立在线存在的人似乎都可以选择无限多的互联网域名注册服务。然而&#xff0c;问题依然存在&#xff1a;哪些提供商更受注册者青睐&#xff1f;WhoisXML API 的研究团队通过分析主要 gTLD&#xff08;通用顶级域&#xff09;和 ccTLD&#xff08;国家或地区顶级域&…

zabbix(二)

zabbix 1.zabbix监控的模式 主动和被动模式都是对于客户端而言 主动模式 客户端主动将数据发送给server或者是代理服务器 被动模式 服务端或者proxy(代理服务器)主动找客户端索要数据------------>默认方式 被动模式在成规模的集群&#xff08;成百上千台的&#xff09;&…

Kubernetes Ingress:流量管理的利器

在 Kubernetes 集群中&#xff0c;服务之间的通信和外部流量的引入通常是至关重要的。虽然 NodePort 和 LoadBalancer 是最常见的解决方案&#xff0c;但当集群内部的服务逐渐增多时&#xff0c;管理不同服务的流量变得复杂。这个时候&#xff0c;Ingress 作为一种强大的流量管…

2012mfc,几种串

串,即是由符组成的串,在标准C,标准C,MFC中串这一功能的实现是不相同的,C完全兼容了C. 1.标准C中的串 在标准C中没有串数据类型,C中的串是有符类型的符数组或符类型的符指针来实现的.如: char name[26]"This is a Cstyle string"; //或char *name"This is a…

任务调度之Quartz(二):Quartz体系结构

1、Quartz 体系结构 由上一篇的Quartz基本使用可以发现&#xff0c;Quartz 主要包含一下几种角色&#xff1a; 1&#xff09;Job&#xff1a;也可以认为是JobDtetail&#xff0c;表示具体的调度任务 2&#xff09;Trigger&#xff1a;触发器&#xff0c;用于定义任务Job出发执行…

十五、Vue 响应接口

文章目录 一、响应式系统基础什么是响应式系统响应式数据的声明与使用二、响应式原理深入Object.defineProperty () 方法的应用(Vue2)Proxy 对象的应用(Vue3)三、响应式接口之 ref 和 reactive(Vue3)ref 函数的使用reactive 函数的使用四、计算属性(computed)作为响应式…

Nature Electronics——近传感器计算:50 nm异构集成技术的革命

创新点&#xff1a;1.高密度互联设计&#xff1a;基于二维材料&#xff0c;开发出互连密度高达62,500 I/O每平方毫米的M3D集成结构。2.异构层堆叠&#xff1a;整合了第二层石墨烯化学传感器和第一层MoS₂记忆晶体管&#xff0c;实现功能互补。3.超短传感器与计算元件距离&#…

使用 SAML 2.0协议需要注意的安全问题

目录 IdP&#xff08;身份提供者&#xff09;方面的安全 SP&#xff08;服务提供者&#xff09;方面的安全 SAML 协议安全 SAML 断言安全 用户身份认证安全 小结 SAML&#xff08;Security Assertion Markup Language&#xff09;2.0 是用于在不同安全域之间交换身份验证…

Ubuntu 安装 Java 1.8

如果你希望使用 Oracle JDK 8&#xff0c;可以按照以下步骤操作&#xff1a; 下载 Oracle JDK 8&#xff1a; 访问 Oracle 官方网站 下载适用于 Ubuntu 的 JDK 8 版本 安装 Oracle JDK 8&#xff1a; 将下载的 JDK 8 压缩包解压到一个目录中&#xff0c;例如 /opt/module&…

字节青训入营考核十五题-Java-找单独的数

问题 问题描述 在一个班级中&#xff0c;每位同学都拿到了一张卡片&#xff0c;上面有一个整数。有趣的是&#xff0c;除了一个数字之外&#xff0c;所有的数字都恰好出现了两次。现在需要你帮助班长小C快速找到那个拿了独特数字卡片的同学手上的数字是什么。 要求&#xff…

Unity学习之UGUI(三)

十二、Slider 1、作用 Slider是滑动条组件&#xff0c;是UGUI中用于处理滑动条相关交互的关键组件 创建Slider默认包括4个对象 父对象&#xff1a;Slider组件依附的对象 子对象&#xff1a;背景图&#xff0c;进度图&#xff0c;滑动块三组对象 2、主要参数 3、代码控制 voi…

win下搭建elk并集成springboot

一、ELK 是什么&#xff1f; ELK 实际上是三个工具的集合&#xff0c;Elasticsearch Logstash Kibana&#xff0c;这三个工具组合形成了一套实用、易用的监控架构&#xff0c;很多公司利用它来搭建可视化的海量日志分析平台。 ElasticSearch ElasticSearch 是一个基于 Lucen…

基于微信小程序疫苗预约系统ssm+论文源码调试讲解

第四章 系统设计 到目前为止&#xff0c;市面上已经存在了各种各样的软件系统&#xff0c;从系统的分类着手&#xff0c;主要应用范围倾向于办公系统&#xff0c;娱乐系统&#xff0c;社交系统&#xff0c;然后下面有很多比较细的分支系统。很多系统已经经过了市场的考验&…