el-upload + 前端直传minio(通过签名地址)
概述
需求
前端请求后端获取预签名url(其中包含了上传到AWS S3所需要的认证信息),然后通过该签名url将文件直传到minio服务器。
问题
minio签名url只支持put请求,使用其他请求会提示错误的请求方法:
前端使用el-upload,默认调用其内部action,通过查看源码,可以看到el-upload组件引用了upload组件,进入upload发现还引入了ajax组件,在ajax组件的upload方法内部,可以看到请求方式是post请求(vue不熟悉,具体原理不清楚,只是站在自己理解的角度):
故通过minio签名url直传方式不能成功。
解决
最后为了使用el-upload,只好折中,引入axios,即el-upload + axios + minio上传方式。
后端生成签名地址
minio具体相关信息在yml文件配置,或者交给nacos管理,不做讨论。
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
| package com.jivehaha.gulimall.thirdparty.controller;
import com.jievhaha.common.utils.Constant; import com.jievhaha.common.utils.R; import io.minio.GetPresignedObjectUrlArgs; import io.minio.MinioClient; import io.minio.http.Method; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID;
@RestController public class MinioController { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.accessKey}") private String accessKey; @Value("${minio.secretKey}") private String secretKey; @Value("${minio.bucket}") private String bucket;
@GetMapping("/minio/policy") private R policy(@RequestParam("pic")String pic) { String name = UUID.randomUUID() + "-" + pic; DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String format = dateTimeFormatter.format(LocalDateTime.now()); String dir = format; pic = dir + "/" + name; String url = ""; String path = "/" + bucket + "/" + pic;
Map<String, String> respMap=null; try {
MinioClient minioClient = MinioClient.builder().endpoint(endpoint) .credentials(accessKey,secretKey).build(); Map<String, String> reqParams = new HashMap<>(); reqParams.put("response-content-type", "application/json"); url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(bucket) .object(pic) .expiry(60 * 60 * 24) .extraQueryParams(reqParams) .build()); System.out.println(url); respMap= new LinkedHashMap<>(); respMap.put("name", name); respMap.put("host", url); respMap.put("path", path); respMap.put("url", Constant.MINIO_URL + path); } catch (Exception e) { System.out.println("Error occurred: " + e); } return R.ok().put("data", respMap); } }
|
el-upload + axios
引入axios
安装axios:
1
| npm install axios --save-dev
|
在main.js引用axios
1 2
| import axios from 'axios'; Vue.prototype.$axios = axios //全局注册,使用方法为:this.$axios
|
前端获取签名url
1 2 3 4 5 6 7 8 9 10 11 12
| import http from '@/utils/httpRequest.js' export function policy(pic) { return new Promise((resolve,reject)=>{ http({ url: http.adornUrl("/thirdparty/minio/policy"), method: "get", params: http.adornParams({pic}) }).then(({ data }) => { resolve(data); }) }); }
|
上传组件
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
| <template> <div> <el-upload action = '' list-type="picture" :multiple="false" :show-file-list="showFileList" :file-list="fileList" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview"> <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div> </el-upload> <el-dialog :visible.sync="dialogVisible"> <img width="100%" :src="fileList[0].url" alt=""> </el-dialog> </div> </template> <script> import {policy} from './policy_minio' export default { name: 'singleUpload', props: { value: String }, computed: { imageUrl() { return this.value; }, imageName() { if (this.value != null && this.value !== '') { return this.value.substring(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) { } } }, data() { return { dialogVisible: false }; }, methods: { emitInput(val) { this.$emit('input', val) }, handleRemove(file, fileList) { this.emitInput(''); }, handlePreview(file) { this.dialogVisible = true; }, beforeUpload(file) // 上传之前先调用policy_minio组件的policy方法获取签名url return new Promise((resolve, reject) => { policy(file.name).then(response => { let url = response.data.url; //将文件名改为后台返回的(原文件名前拼了段uuid),不然同名文件会覆盖 let newFileName = response.data.name; let imageType = "image/" + newFileName.substring(newFileName.lastIndexOf(".") + 1); let newFile = new File([file], response.data.name,{type:imageType}); this.$axios.request({ url: response.data.host, method: 'put', data: newFile }).then((res)=> { this.showFileList = true; this.fileList.pop(); this.fileList.push({name: file.name, url: url}); this.emitInput(this.fileList[0].url); }).catch(()=> { console.log("响应数据:上传失败"); })
}).catch(err => { console.log(JSON.stringify(err)); reject(false) }) }) }, handleUploadSuccess(res, file) { } } } </script> <style>
</style>
|
着重点在beforeUpload方法,上传之前先调用policy_minio组件的policy方法获取签名url,然后再通过axios组件上传(假如使用FormData对象传递,最后会发现上传成功后,文件大小改变,且打开失败,故直接上传文件对象)。
效果
通过组件上传,上传成功预览。
上传成功后服务器文件。
注意:需要正常展示下载文件需要修改对应bucket权限,否则直接通过文件在服务器的路径访问,会直接进入文件夹,新增read and write权限。