我們傳統使用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 {@Autowiredprivate MinIoProperties properties;@Beanpublic 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 ++] 清除默認baseURLheaders: {'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:調用后端接口生成臨時有效的下載URLconst { data: { data: presignedUrl } } = await getPresignedDownloadUrl(fileName);// 2. 創建隱藏鏈接觸發下載const link = document.createElement('a');link.href = presignedUrl; // 設置URLlink.download = fileName; // 設置下載文件名,需與 MinIO 存儲的文件名一致。document.body.appendChild(link); // 將鏈接添加到DOMlink.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><inputv-model="downloadFileName"type="text"placeholder="請輸入文件名(如 test.jpg)"/><button @click="downloadFile">下載</button><p>{{ downloadStatus }}</p></div> </template>
上傳測試結果:
?
下載測試: