# 阿里云 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
```
