449 lines
14 KiB
Markdown
449 lines
14 KiB
Markdown
|
|
# 阿里云 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
|
|||
|
|
```
|