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

449 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 阿里云 OSS 前端直传 — 迁移文档
> 将本项目中的「阿里云 OSS 前端直传」功能提取为通用方案,方便迁移到其他项目。
---
## 目录
- [整体架构](#整体架构)
- [后端实现原理](#后端实现原理)
- [前端实现原理](#前端实现原理)
- [迁移步骤](#迁移步骤)
- [后端迁移5 步)](#后端迁移5-步)
- [前端迁移3 步)](#前端迁移3-步)
- [配置说明](#配置说明)
- [注意事项与安全建议](#注意事项与安全建议)
- [验证方法](#验证方法)
- [文件清单](#文件清单)
---
## 整体架构
### 时序图
```
┌────────┐ ┌────────┐ ┌──────────────┐
│ 前端 │ │ 后端 │ │ 阿里云 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
```json
{
"expiration": "2026-04-08T12:00:00.000Z",
"conditions": [
["eq", "$key", "dev/avatar/2026-04-08/a1b2c3d4.jpg"]
]
}
```
- `expiration`Token 过期时间ISO 8601 格式)
- `conditions`:约束条件,这里限定只能上传到指定的 key文件路径
#### 2. Base64 编码 Policy
```
eyJleHBpcmF0aW9uIjoiMjAyNi0wNC0wOFQxMjowMDowMC4wMDBaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIka2V5IiwiZGV2L2F2YXRhci8yMDI2LTA0LTA4L2ExYjJjM2Q0LmpwZyJdXX0=
```
#### 3. HMAC-SHA1 签名
```
signature = Base64(HMAC-SHA1(Base64(policy), accessKeySecret))
```
#### 4. 返回给前端
```json
{
"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
```typescript
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`
```xml
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.1</version>
</dependency>
```
> 至少需要:`aliyun-sdk-oss`、`lombok`、`spring-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`(或对应环境的配置文件):
```yaml
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 步:验证后端
启动项目后,访问以下接口验证:
```bash
curl "http://localhost:8080/api/v1/files/oss/token?fileName=test.jpg&dir=avatar"
```
期望返回:
```json
{
"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 地址:
```typescript
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";
```
如果使用自定义的 HTTP 封装(如项目中已有 `http` 工具),可以将 `axios.get` 替换为你的封装:
```typescript
// 原始写法
const response = await axios.get(`${API_BASE_URL}/v1/files/oss/token`, ...)
// 替换为你的 HTTP 封装
const response = await http.get(`/v1/files/oss/token`, ...)
```
#### 第 3 步:在组件中使用
```vue
<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 控制台](https://oss.console.aliyun.com/),创建 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 子账号,仅授予必要权限:
```json
{
"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 接口
```bash
curl "http://localhost:8080/api/v1/files/oss/token?fileName=test.jpg&dir=test"
```
确认返回的 JSON 包含 `accessid`、`policy`、`signature`、`key`、`host` 字段。
### 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
```