MinIO的预签名直传机制
我们传统使用MinIo做OSS对象存储的应用方式往往都是在后端配置与MinIO的连接和文件上传下载的相关接口,然后我们在前端调用这些接口完成文件的上传下载机制,但是,当并发量过大,频繁访问会对后端的并发往往会对服务器造成极大的压力,大文件传输场景下,服务器被迫承担数据中转的角色,既消耗大量带宽资源,又形成单点性能瓶颈。这时,我们引入了MinIO的一种预签名机制。
预签名机制:在后端对文件的上传和下载操作生成一个URL,前端针对不同的文件操作形式请求会获取到对应的URL,这个URL可以理解为一个临时的通行证,有了这个URL后,前端可以直接向MinIO的服务端发上传和下载的相应请求,与MinIO直连操作,大大减缓了对后端服务器的压力
1.后端配置
1.1 引入Maven依赖并配置MinIO
<!--minio--> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> </dependency>
/* * MinIO配置类 * @Author GuihaoLv */ @Configuration @EnableConfigurationProperties(MinIoProperties.class) public class MinIoConfiguration { @Autowired private MinIoProperties properties; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(properties.getEndpoint()) .credentials(properties.getAccessKey(), properties.getSecretKey()) .build(); } }
1.2 生成预签名接口封装:
/**要改成使用预签名URL,让前端直接与MinIO交互,减轻服务器负担。 * 生成上传预签名URL(PUT) * @param fileName * @return */ @GetMapping("/presigned-upload-url") @ApiOperation("获取上传预签名URL") public Result<String> generateUploadUrl(@RequestParam("fileName") String fileName) { System.out.println("测试"+fileName); String url = commonFileService.generatePresignedUploadUrl(fileName); System.out.println("结构"+url); return Result.success(url); } /**要改成使用预签名URL,让前端直接与MinIO交互,减轻服务器负担。 * 生成下载预签名URL(GET) * @param fileName * @return */ @GetMapping("/presigned-download-url") @ApiOperation("获取下载预签名URL") public Result<String> generateDownloadUrl(@RequestParam("fileName") String fileName) { String url = commonFileService.generatePresignedDownloadUrl(fileName); return Result.success(url); }
/** 生成上传预签名URL(PUT) * @param fileName * @return */ public String generatePresignedUploadUrl(String fileName) { try { // 安全处理文件名(防止路径遍历) String safeFileName = sanitizeFileName(fileName); // 生成预签名URL(PUT方法) return client.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(properties.getBucketName()) .object(safeFileName) .expiry(15, TimeUnit.MINUTES) // 15分钟有效 .build() ); } catch (Exception e) { throw new RuntimeException("生成预签名URL失败", e); } } /** * 生成下载预签名URL(GET) * @param fileName * @return */ public String generatePresignedDownloadUrl(String fileName) { try { String safeFileName = sanitizeFileName(fileName); return client.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(properties.getBucketName()) .object(safeFileName) .expiry(1, TimeUnit.HOURS) // 1小时有效 .build() ); } catch (Exception e) { throw new RuntimeException("生成预签名URL失败", e); } } // 文件名安全处理 private String sanitizeFileName(String fileName) { // 过滤非法字符,防止路径遍历 return fileName.replaceAll("[^a-zA-Z0-9-_.]", ""); }
1.3 前端封装获取预签名和直连MinIO做上传下载的请求
// 获取上传预签名URL export const getPresignedUploadUrl = (fileName) => { return httpInstance({ url: '/web/commonFile/presigned-upload-url', method: 'GET', params: { fileName }, }); }; // 获取下载预签名URL export const getPresignedDownloadUrl = (fileName) => { return httpInstance({ url: '/web/commonFile/presigned-download-url', method: 'GET', params: { fileName }, }); }; // 单个文件直传MinIO,上传文件 export const uploadViaPresignedUrl = async (file: File) => { try { // 步骤1: 获取未编码的原始文件名(需与后端生成的签名匹配) const rawFileName = file.name; // 步骤2: 调用后端接口获取预签名URL(必须传递原始文件名) const res=await getPresignedUploadUrl(rawFileName); const presignedUrl=res.data; // 调试输出:验证URL格式 console.log('[DEBUG] 预签名URL:', presignedUrl); // 应输出类似 http://47.99.49.193:9000/... // 步骤3: 直接向MinIO发送PUT请求(绕过代理) const response = await axios.put(presignedUrl, file, { // 关键配置:禁用代理和默认请求头 baseURL: '', // [!code ++] 清除默认baseURL headers: { 'Content-Type': 'application/octet-stream' // MinIO通用类型 } }); return response.data; } catch (error) { throw new Error(`上传失败: ${(error).response?.data || error.message}`); } }; // 使用预签名URL直连MinIO下载文件 export const downloadViaPresignedUrl = async (fileName) => { try { // 1. 获取预签名URL:调用后端接口生成临时有效的下载URL const { data: { data: presignedUrl } } = await getPresignedDownloadUrl(fileName); // 2. 创建隐藏链接触发下载 const link = document.createElement('a'); link.href = presignedUrl; // 设置URL link.download = fileName; // 设置下载文件名,需与 MinIO 存储的文件名一致。 document.body.appendChild(link); // 将链接添加到DOM link.click(); // 模拟点击触发下载 document.body.removeChild(link); // 移除临时链接 return true; // 表示下载已触发 } catch (error) { throw new Error('下载失败: ' + error.message); // 统一错误处理 } };
1.4:写一个前端页面测试前端直连MinIO的功能实现
<script setup lang="ts"> import { ref } from 'vue'; import { uploadViaPresignedUrl, downloadViaPresignedUrl } from '@/api/file'; // 定义响应式变量 const selectedFile = ref<File | null>(null); // 存储用户选择的文件 const downloadFileName = ref<string>(''); // 下载时输入的文件名 const uploadStatus = ref<string>(''); // 上传状态提示 const downloadStatus = ref<string>(''); // 下载状态提示 // 处理文件选择事件 const handleFileChange = (event: Event) => { const target = event.target as HTMLInputElement; if (target.files && target.files.length > 0) { selectedFile.value = target.files[0]; uploadStatus.value = ''; // 重置上传状态 } }; // 上传文件到MinIO const uploadFile = async () => { uploadStatus.value = '上传中...'; await uploadViaPresignedUrl(selectedFile.value); uploadStatus.value = '上传成功!'; selectedFile.value = null; // 清空文件选择 } // 下载文件从MinIO const downloadFile = async () => { downloadStatus.value = '正在触发下载...'; const success = await downloadViaPresignedUrl(downloadFileName.value); if (success) { downloadStatus.value = '下载已触发!'; } }; </script> <template> <div class="container"> <!-- 上传文件部分 --> <h2>测试MinIO文件上传</h2> <input type="file" @change="handleFileChange" /> <button @click="uploadFile" :disabled="!selectedFile">上传</button> <p>{{ uploadStatus }}</p> <!-- 下载文件部分 --> <h2>测试MinIO文件下载</h2> <input v-model="downloadFileName" type="text" placeholder="请输入文件名(如 test.jpg)" /> <button @click="downloadFile">下载</button> <p>{{ downloadStatus }}</p> </div> </template>
上传测试结果:
下载测试: