357 lines
8.4 KiB
Markdown
357 lines
8.4 KiB
Markdown
|
|
# COS 预签名 URL 直传指南(推荐方式)
|
|||
|
|
|
|||
|
|
## 🎯 为什么使用预签名 URL?
|
|||
|
|
|
|||
|
|
相比 POST 表单上传,预签名 URL 直传更简单:
|
|||
|
|
- ✅ **无需复杂的表单字段** - 只需要一个 URL
|
|||
|
|
- ✅ **前端代码更简洁** - 直接 PUT 文件即可
|
|||
|
|
- ✅ **自动处理 CORS** - 预签名 URL 包含签名,不受 CORS 限制
|
|||
|
|
- ✅ **更安全** - URL 有过期时间,临时授权
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 完整的前端上传代码
|
|||
|
|
|
|||
|
|
### Vue 3 + Element Plus 示例
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<el-upload
|
|||
|
|
:action="uploadUrl"
|
|||
|
|
:before-upload="beforeUpload"
|
|||
|
|
:http-request="customUpload"
|
|||
|
|
:show-file-list="false"
|
|||
|
|
accept="image/*"
|
|||
|
|
>
|
|||
|
|
<el-button type="primary">选择图片</el-button>
|
|||
|
|
</el-upload>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref } from 'vue';
|
|||
|
|
import { ElMessage } from 'element-plus';
|
|||
|
|
|
|||
|
|
const uploadUrl = ref('');
|
|||
|
|
const fileUrl = ref('');
|
|||
|
|
|
|||
|
|
// 上传前获取预签名 URL
|
|||
|
|
const beforeUpload = async (file) => {
|
|||
|
|
try {
|
|||
|
|
// 1. 获取预签名 URL
|
|||
|
|
const response = await fetch('/user/oss/presigned-url', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
fileName: file.name,
|
|||
|
|
userId: '123' // 从登录状态获取
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await response.json();
|
|||
|
|
if (result.code === 200) {
|
|||
|
|
uploadUrl.value = result.data.uploadUrl;
|
|||
|
|
fileUrl.value = result.data.fileUrl;
|
|||
|
|
return true;
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(result.message);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取预签名URL失败:', error);
|
|||
|
|
ElMessage.error('获取上传地址失败');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 自定义上传方法
|
|||
|
|
const customUpload = async ({ file }) => {
|
|||
|
|
try {
|
|||
|
|
// 2. 直接 PUT 文件到预签名 URL
|
|||
|
|
const response = await fetch(uploadUrl.value, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: file,
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': file.type
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
ElMessage.success('上传成功');
|
|||
|
|
console.log('文件访问地址:', fileUrl.value);
|
|||
|
|
// 3. 使用 fileUrl.value 更新头像等
|
|||
|
|
return { url: fileUrl.value };
|
|||
|
|
} else {
|
|||
|
|
throw new Error('上传失败');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('上传失败:', error);
|
|||
|
|
ElMessage.error('上传失败');
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 原生 JavaScript 示例
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
async function uploadFile(file, userId) {
|
|||
|
|
try {
|
|||
|
|
// 1. 获取预签名 URL
|
|||
|
|
const signResponse = await fetch('/user/oss/presigned-url', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
fileName: file.name,
|
|||
|
|
userId: userId
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const signResult = await signResponse.json();
|
|||
|
|
if (signResult.code !== 200) {
|
|||
|
|
throw new Error(signResult.message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { uploadUrl, fileUrl } = signResult.data;
|
|||
|
|
|
|||
|
|
// 2. 直接 PUT 文件到预签名 URL
|
|||
|
|
const uploadResponse = await fetch(uploadUrl, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: file,
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': file.type
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!uploadResponse.ok) {
|
|||
|
|
throw new Error('上传失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 上传成功,返回文件访问地址
|
|||
|
|
console.log('上传成功,文件地址:', fileUrl);
|
|||
|
|
return fileUrl;
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('上传失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用示例
|
|||
|
|
document.getElementById('fileInput').addEventListener('change', async (e) => {
|
|||
|
|
const file = e.target.files[0];
|
|||
|
|
if (file) {
|
|||
|
|
try {
|
|||
|
|
const fileUrl = await uploadFile(file, '123');
|
|||
|
|
alert('上传成功: ' + fileUrl);
|
|||
|
|
} catch (error) {
|
|||
|
|
alert('上传失败: ' + error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### React 示例
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
import { useState } from 'react';
|
|||
|
|
import { message } from 'antd';
|
|||
|
|
|
|||
|
|
function FileUpload() {
|
|||
|
|
const [uploading, setUploading] = useState(false);
|
|||
|
|
|
|||
|
|
const handleUpload = async (file) => {
|
|||
|
|
setUploading(true);
|
|||
|
|
try {
|
|||
|
|
// 1. 获取预签名 URL
|
|||
|
|
const signResponse = await fetch('/user/oss/presigned-url', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
fileName: file.name,
|
|||
|
|
userId: '123'
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const signResult = await signResponse.json();
|
|||
|
|
if (signResult.code !== 200) {
|
|||
|
|
throw new Error(signResult.message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { uploadUrl, fileUrl } = signResult.data;
|
|||
|
|
|
|||
|
|
// 2. PUT 文件到预签名 URL
|
|||
|
|
const uploadResponse = await fetch(uploadUrl, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: file,
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': file.type
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!uploadResponse.ok) {
|
|||
|
|
throw new Error('上传失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message.success('上传成功');
|
|||
|
|
console.log('文件地址:', fileUrl);
|
|||
|
|
return fileUrl;
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('上传失败:', error);
|
|||
|
|
message.error('上传失败: ' + error.message);
|
|||
|
|
} finally {
|
|||
|
|
setUploading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<input
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
onChange={(e) => handleUpload(e.target.files[0])}
|
|||
|
|
disabled={uploading}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📡 API 接口说明
|
|||
|
|
|
|||
|
|
### 接口地址
|
|||
|
|
```
|
|||
|
|
POST /user/oss/presigned-url
|
|||
|
|
GET /user/oss/presigned-url
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 请求参数(POST 方式)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"fileName": "avatar.jpg",
|
|||
|
|
"userId": "123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 响应数据
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"message": "预签名URL生成成功",
|
|||
|
|
"data": {
|
|||
|
|
"uploadUrl": "https://oss-1818ai-user-img-1302947942.cos.ap-guangzhou.myqcloud.com/user_img/xxx.jpg?sign=...",
|
|||
|
|
"fileUrl": "https://oss-1818ai-user-img-1302947942.cos.ap-guangzhou.myqcloud.com/user_img/xxx.jpg",
|
|||
|
|
"fileName": "avatar.jpg",
|
|||
|
|
"expiresIn": 3600
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 字段说明
|
|||
|
|
|
|||
|
|
| 字段 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| `uploadUrl` | 预签名上传 URL(包含签名,有效期1小时) |
|
|||
|
|
| `fileUrl` | 文件访问地址(永久有效) |
|
|||
|
|
| `fileName` | 文件名 |
|
|||
|
|
| `expiresIn` | 签名有效期(秒) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 上传流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────┐ 1. 请求预签名URL ┌─────────┐
|
|||
|
|
│ │ ────────────────────────> │ │
|
|||
|
|
│ 前端 │ │ 后端 │
|
|||
|
|
│ │ <──────────────────────── │ │
|
|||
|
|
└─────────┘ 2. 返回预签名URL └─────────┘
|
|||
|
|
│
|
|||
|
|
│ 3. PUT 文件到预签名URL
|
|||
|
|
↓
|
|||
|
|
┌─────────┐
|
|||
|
|
│ COS │
|
|||
|
|
│ 存储桶 │
|
|||
|
|
└─────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 优势对比
|
|||
|
|
|
|||
|
|
| 特性 | 预签名 URL 直传 | POST 表单上传 |
|
|||
|
|
|------|----------------|--------------|
|
|||
|
|
| **前端代码** | ✅ 简单(一个 PUT 请求) | ❌ 复杂(多个表单字段) |
|
|||
|
|
| **CORS 配置** | ✅ 不需要(签名在 URL 中) | ❌ 需要配置 |
|
|||
|
|
| **字段匹配** | ✅ 无需关心字段名 | ❌ 必须匹配 COS 字段名 |
|
|||
|
|
| **错误排查** | ✅ 简单 | ❌ 复杂(403 签名错误) |
|
|||
|
|
| **安全性** | ✅ URL 有过期时间 | ✅ Policy 有过期时间 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚠️ 注意事项
|
|||
|
|
|
|||
|
|
1. **预签名 URL 有效期**
|
|||
|
|
- 默认 1 小时(3600 秒)
|
|||
|
|
- 过期后需要重新获取
|
|||
|
|
|
|||
|
|
2. **Content-Type**
|
|||
|
|
- PUT 请求时建议设置正确的 Content-Type
|
|||
|
|
- 例如:`image/jpeg`, `image/png`
|
|||
|
|
|
|||
|
|
3. **文件大小限制**
|
|||
|
|
- 默认最大 100MB
|
|||
|
|
- 可在后端配置中调整
|
|||
|
|
|
|||
|
|
4. **文件命名**
|
|||
|
|
- 后端会自动生成唯一文件名(UUID)
|
|||
|
|
- 避免文件名冲突
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧪 测试命令
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. 获取预签名 URL
|
|||
|
|
curl -X POST http://localhost:8083/user/oss/presigned-url \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|||
|
|
-d '{"fileName":"test.jpg","userId":"123"}'
|
|||
|
|
|
|||
|
|
# 2. 使用预签名 URL 上传文件
|
|||
|
|
curl -X PUT "<返回的uploadUrl>" \
|
|||
|
|
-H "Content-Type: image/jpeg" \
|
|||
|
|
--data-binary "@/path/to/test.jpg"
|
|||
|
|
|
|||
|
|
# 3. 访问文件
|
|||
|
|
curl "<返回的fileUrl>"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎉 总结
|
|||
|
|
|
|||
|
|
使用预签名 URL 直传是最简单、最可靠的方式:
|
|||
|
|
- ✅ 前端只需两步:获取 URL → PUT 文件
|
|||
|
|
- ✅ 无需配置 CORS
|
|||
|
|
- ✅ 无需关心复杂的表单字段
|
|||
|
|
- ✅ 错误排查简单
|
|||
|
|
|
|||
|
|
**强烈推荐使用这种方式!**
|