Files
1818web-hoduan/COS_PRESIGNED_URL_UPLOAD_GUIDE.md
Claude Workbench e3e6f1f29d first commit
2026-02-13 18:18:20 +08:00

8.4 KiB
Raw Permalink Blame History

COS 预签名 URL 直传指南(推荐方式)

🎯 为什么使用预签名 URL

相比 POST 表单上传,预签名 URL 直传更简单:

  • 无需复杂的表单字段 - 只需要一个 URL
  • 前端代码更简洁 - 直接 PUT 文件即可
  • 自动处理 CORS - 预签名 URL 包含签名,不受 CORS 限制
  • 更安全 - URL 有过期时间,临时授权

📋 完整的前端上传代码

Vue 3 + Element Plus 示例

<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 示例

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 示例

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 方式)

{
  "fileName": "avatar.jpg",
  "userId": "123"
}

响应数据

{
  "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
    • 避免文件名冲突

🧪 测试命令

# 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
  • 无需关心复杂的表单字段
  • 错误排查简单

强烈推荐使用这种方式!