library-picturebook-activity/oss-direct-upload-demo/README.md
En b9ed5e17c6 feat: OSS 客户端直传改造(STS Token 签发 + 前端直传 + CORS 自动配置)
后端新增 OssUtils/OssTokenVo/OssCorsInitRunner,通过 STS 临时凭证实现客户端直传 OSS;
前端 upload API 适配直传流程,赛事创建/作品提交/作业/富文本编辑器均已切换;
多环境(dev/test/prod) OSS 配置补全;新增 oss-direct-upload-demo 示例项目及 E2E 测试。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:19:43 +08:00

14 KiB
Raw Blame History

阿里云 OSS 前端直传 — 迁移文档

将本项目中的「阿里云 OSS 前端直传」功能提取为通用方案,方便迁移到其他项目。


目录


整体架构

时序图

┌────────┐          ┌────────┐          ┌──────────────┐
│  前端   │          │  后端   │          │ 阿里云 OSS    │
└───┬────┘          └───┬────┘          └──────┬───────┘
    │                   │                      │
    │  ① GET /oss/token │                      │
    │  ?fileName=图片.jpg│                      │
    │  &dir=dev/avatar  │                      │
    │──────────────────>│                      │
    │                   │                      │
    │                   │  ② 生成签名 Token     │
    │                   │  - Policy (Base64)   │
    │                   │  - Signature (HMAC)  │
    │                   │  - Key (文件路径)     │
    │                   │                      │
    │  ③ 返回 Token     │                      │
    │  {accessid,       │                      │
    │   policy,         │                      │
    │   signature,      │                      │
    │   key, host}      │                      │
    │<──────────────────│                      │
    │                   │                      │
    │  ④ POST FormData 直传(不经过后端)        │
    │  ┌──────────────────────────────────────┐│
    │  │ FormData:                            ││
    │  │  - success_action_status: 200        ││
    │  │  - OSSAccessKeyId: {accessid}        ││
    │  │  - policy: {policy}                  ││
    │  │  - signature: {signature}            ││
    │  │  - key: {key}                        ││
    │  │  - file: (二进制文件)                  ││
    │  └──────────────────────────────────────┘│
    │─────────────────────────────────────────>│
    │                   │                      │
    │  ⑤ 返回 200 OK    │                      │
    │<─────────────────────────────────────────│
    │                   │                      │
    │  ⑥ 使用文件 URL    │                      │
    │  https://bucket    │                      │
    │  .oss-cn-xxx       │                      │
    │  .aliyuncs.com     │                      │
    │  /dev/avatar/      │                      │
    │  2026-04-08/       │                      │
    │  {uuid}.jpg        │                      │
    │─────────────────────────────────────────>│
    │  ⑦ 返回文件         │                      │
    │<─────────────────────────────────────────│
    │                   │                      │

核心优势

特性 说明
零后端带宽 文件直接传到 OSS后端仅生成签名 Token
安全性 AccessKeySecret 只在后端,前端只拿到临时签名
高性能 利用阿里云 CDN 加速,上传速度快
可扩展 支持进度回调、取消上传、超时控制
环境隔离 自动添加 dev/test/prod 前缀,避免文件冲突

后端实现原理

PostObject 签名机制

阿里云 OSS 前端直传使用的是 PostObject 方式,核心是签名机制:

1. 构建 Policy

{
  "expiration": "2026-04-08T12:00:00.000Z",
  "conditions": [
    ["eq", "$key", "dev/avatar/2026-04-08/a1b2c3d4.jpg"]
  ]
}
  • expirationToken 过期时间ISO 8601 格式)
  • conditions:约束条件,这里限定只能上传到指定的 key文件路径

2. Base64 编码 Policy

eyJleHBpcmF0aW9uIjoiMjAyNi0wNC0wOFQxMjowMDowMC4wMDBaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIka2V5IiwiZGV2L2F2YXRhci8yMDI2LTA0LTA4L2ExYjJjM2Q0LmpwZyJdXX0=

3. HMAC-SHA1 签名

signature = Base64(HMAC-SHA1(Base64(policy), accessKeySecret))

4. 返回给前端

{
  "accessid": "LTAI5tXXXXXX",
  "policy": "Base64 编码的 Policy",
  "signature": "Base64 编码的签名",
  "dir": "dev/avatar/",
  "host": "https://your-bucket.oss-cn-hangzhou.aliyuncs.com",
  "key": "dev/avatar/2026-04-08/a1b2c3d4.jpg",
  "expire": 30
}

前端实现原理

FormData 直传

前端拿到 Token 后,使用 FormData 构造表单,直接 POST 到 OSS

const formData = new FormData();
formData.append("success_action_status", "200");  // 成功返回 200
formData.append("OSSAccessKeyId", token.accessid);
formData.append("policy", token.policy);
formData.append("signature", token.signature);
formData.append("key", token.key);
formData.append("file", file);  // file 必须为最后一个表单域

await axios.post(token.host, formData, {
  headers: { "Content-Type": "multipart/form-data" },
  onUploadProgress: (e) => { /* 进度回调 */ },
});

环境目录自动隔离

env.ts 根据当前环境自动添加前缀:

开发环境: dev/avatar/2026-04-08/{uuid}.jpg
测试环境: test/avatar/2026-04-08/{uuid}.jpg
生产环境: prod/avatar/2026-04-08/{uuid}.jpg

迁移步骤

后端迁移5 步)

第 1 步:添加 Maven 依赖

pom-oss.xml 中的依赖复制到你的 pom.xml

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.1</version>
</dependency>

至少需要:aliyun-sdk-osslombokspring-boot-starter-web

第 2 步:复制 Java 文件

将以下 4 个文件复制到你的项目中(根据包名调整):

文件 放置位置 说明
OssConfig.java com/xxx/config/ 配置类,绑定 yml 配置
OssTokenVo.java com/xxx/vo/ Token 响应对象
OssUtils.java com/xxx/util/ 核心工具类(签名 + CORS
FileUploadController.java com/xxx/controller/ API 接口

可选:OssCorsInitRunner.java — 如果希望启动时自动配置 CORS

第 3 步:修改包名

全局搜索替换包名:

com.example.oss.config     →  你的包名.config
com.example.oss.vo         →  你的包名.vo
com.example.oss.util       →  你的包名.util
com.example.oss.controller →  你的包名.controller

第 4 步:添加配置

application-oss.yml 中的配置复制到你的 application.yml(或对应环境的配置文件):

aliyun:
  oss:
    endpoint: ${OSS_ENDPOINT:oss-cn-hangzhou.aliyuncs.com}
    access-key-id: ${OSS_ACCESS_KEY_ID:your-key}
    access-key-secret: ${OSS_ACCESS_KEY_SECRET:your-secret}
    bucket-name: ${OSS_BUCKET_NAME:your-bucket}
    max-file-size: ${OSS_MAX_FILE_SIZE:10485760}
    cors-enabled: ${OSS_CORS_ENABLED:true}
    cors-allowed-origins: ${OSS_CORS_ORIGINS:http://localhost:5173}

第 5 步:验证后端

启动项目后,访问以下接口验证:

curl "http://localhost:8080/api/v1/files/oss/token?fileName=test.jpg&dir=avatar"

期望返回:

{
  "code": 200,
  "data": {
    "accessid": "LTAI5tXXXXXX",
    "policy": "...",
    "signature": "...",
    "dir": "avatar/",
    "host": "https://your-bucket.oss-cn-hangzhou.aliyuncs.com",
    "key": "avatar/2026-04-08/xxxxxxxx.jpg",
    "expire": 30
  }
}

前端迁移3 步)

第 1 步:复制 TypeScript 文件

将以下 2 个文件复制到你的项目中:

文件 放置位置 说明
file.ts src/api/src/utils/ 上传 API
env.ts src/utils/ 环境工具

第 2 步:修改配置

file.ts 中修改后端 API 地址:

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";

如果使用自定义的 HTTP 封装(如项目中已有 http 工具),可以将 axios.get 替换为你的封装:

// 原始写法
const response = await axios.get(`${API_BASE_URL}/v1/files/oss/token`, ...)

// 替换为你的 HTTP 封装
const response = await http.get(`/v1/files/oss/token`, ...)

第 3 步:在组件中使用

<script setup lang="ts">
import { uploadFile } from "@/api/file";

async function handleUpload(file: File) {
  const result = await uploadFile(file, "avatar", {
    onProgress: (percent) => console.log(`上传进度: ${percent}%`),
  });
  console.log("上传成功:", result.filePath);
}
</script>

参考 UploadDemo.vue 查看完整的组件示例。


配置说明

阿里云 OSS Bucket 配置

1. 创建 Bucket

登录 阿里云 OSS 控制台,创建 Bucket

  • Bucket 名称:自定义(如 my-project-files
  • 地域:选择离用户最近的节点
  • 存储类型:标准存储
  • 读写权限:公共读(文件上传后可直接通过 URL 访问)

2. CORS 配置

如果后端 cors-enabled 设为 true,启动时会自动配置。否则需要手动配置:

在 OSS 控制台 → Bucket → 权限管理 → 跨域设置:

配置项
允许来源 http://localhost:5173(开发)/ https://your-domain.com(生产)
允许方法 GET, POST, PUT, DELETE, HEAD
允许 Headers *
暴露 Headers ETag, x-oss-request-id
缓存时间 600 秒

3. RAM 权限

建议为应用创建独立的 RAM 子账号,仅授予必要权限:

{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:PutObject",
        "oss:GetObject",
        "oss:DeleteObject",
        "oss:PutBucketCors"
      ],
      "Resource": [
        "acs:oss:*:*:your-bucket-name",
        "acs:oss:*:*:your-bucket-name/*"
      ]
    }
  ],
  "Version": "1"
}

环境变量

变量名 必填 默认值 说明
OSS_ENDPOINT - OSS Endpoint
OSS_ACCESS_KEY_ID - 阿里云 AccessKey ID
OSS_ACCESS_KEY_SECRET - 阿里云 AccessKey Secret
OSS_BUCKET_NAME - Bucket 名称
OSS_MAX_FILE_SIZE 10485760 (10MB) 文件大小限制
OSS_CORS_ENABLED true 是否自动配置 CORS
OSS_CORS_ORIGINS http://localhost:5173 CORS 允许的来源

注意事项与安全建议

安全

  1. AccessKeySecret 永远不要暴露给前端:签名只在后端计算,前端只拿到签名结果
  2. Token 有效期很短30 秒):防止 Token 被盗用
  3. Policy 中限制了 key:前端只能上传到指定的路径,无法覆盖其他文件
  4. 使用 RAM 子账号:不要使用主账号的 AccessKey
  5. 生产环境使用环境变量:不要在配置文件中硬编码密钥

注意事项

  1. 文件名生成:后端使用 UUID + 原扩展名 生成唯一文件名,避免文件名冲突
  2. 日期分区:文件按日期分目录存储(如 avatar/2026-04-08/),方便管理
  3. 环境前缀:前端自动添加 dev/test/prod 前缀,确保不同环境的文件互不干扰
  4. CORS 配置
    • 方式一:后端启动时自动配置(需 oss:PutBucketCors 权限)
    • 方式二:在阿里云控制台手动配置(推荐生产环境)
  5. file 字段位置FormData 中 file 必须为最后一个字段,否则 OSS 可能报错

常见问题

问题 原因 解决方案
CORS 报错 OSS Bucket 未配置 CORS 开启 cors-enabled 或手动配置
403 Forbidden 签名过期或错误 检查 AccessKey、系统时间
InvalidPolicy Policy 格式错误 确认 ISO 8601 时间格式以 Z 结尾
文件上传成功但无法访问 Bucket 读写权限为私有 设置为公共读或使用签名 URL
FormData 报错 file 字段不在最后 确保 formData.append("file", ...) 在最后

验证方法

1. 验证后端 Token 接口

curl "http://localhost:8080/api/v1/files/oss/token?fileName=test.jpg&dir=test"

确认返回的 JSON 包含 accessidpolicysignaturekeyhost 字段。

2. 验证前端直传

使用 UploadDemo.vue 组件,选择一个图片文件,点击上传:

  • 进度条应正常显示
  • 上传成功后显示文件 URL
  • 在浏览器中访问 URL 可看到文件

3. 验证 CORS

在浏览器控制台中检查:

  • 上传请求不应出现 CORS 报错
  • OPTIONS 预检请求应返回 200

4. 验证环境隔离

分别在 development 和 production 环境上传文件,确认:

  • 开发环境文件在 dev/ 目录下
  • 生产环境文件在 prod/ 目录下

文件清单

docs/oss-direct-upload-demo/
├── README.md                          ← 你正在看的文档
├── backend/
│   ├── OssConfig.java                 ← 配置类(绑定 yml 配置)
│   ├── OssTokenVo.java                ← Token 响应对象
│   ├── OssUtils.java                  ← 核心工具类(签名 + CORS
│   ├── FileUploadController.java      ← Controller仅获取 Token 接口)
│   ├── OssCorsInitRunner.java         ← 启动时自动配置 CORS可选
│   ├── application-oss.yml            ← 配置文件示例
│   └── pom-oss.xml                    ← Maven 依赖片段
└── frontend/
    ├── file.ts                        ← 文件上传 API类型 + 直传逻辑)
    ├── env.ts                         ← 环境目录前缀工具
    └── UploadDemo.vue                 ← 上传组件 Demo