在Koa.js中实现文件上传的接口(服务端签名直传)
目录
[TOC]
为什么客户端直传
在典型的服务端和客户端架构下,常见的文件上传方式是服务端代理上传:客户端将文件上传到业务服务器,然后业务服务器将文件上传到OSS。在这个过程中,一份数据需要在网络上传输两次,会造成网络资源的浪费,增加服务端的资源开销。为了解决这一问题,您可以在客户端直连OSS来完成文件上传,无需经过业务服务器中转。
服务端生成签名以实现Web端直传,使用户能够通过Web浏览器直接使用 PostObject 接口上传文件到OSS。此过程通过签名机制确保上传的安全性。同时,根据具体业务需求,您可以在服务端生成上传策略(Policy),以限制上传操作。
方案概览
服务端生成签名实现Web端直传的过程如下:
要实现服务端签名直传,只需3步:
说明
对于需要限制上传文件属性的场景,您可以在服务端生成PostObject所需的Post签名、PostPolicy等信息,然后客户端可以凭借这些信息,在一定的限制下不依赖OSS SDK直接上传文件。您可以借助服务端生成的PostPolicy限制客户端上传的文件,例如限制文件大小、文件类型。此方案适用于通过HTML表单上传的方式上传文件。需要注意的是,此方案不支持基于分片上传大文件、基于分片断点续传的场景。
-
配置OSS :配置OSS,在控制台创建一个Bucket,用于存储用户上传的文件。同时,为 Bucket 配置跨域资源共享(CORS) 规则,以允许来自服务端的跨域请求。
-
配置服务端: 配置服务端,服务端生成PostObject所需的签名和Post Policy,然后使用访问凭证和服务端预设的上传策略(如Bucket名称、目录路径、过期时间等)生成签名授权用户在一定时间内进行文件上传。
-
配置Web端 :配置Web端,构造HTML表单请求,通过表单提交使用签名将文件上传到OSS。
步骤一:配置OSS
一、创建Bucket
创建一个OSS Bucket,用于存储Web应用在浏览器环境中直接上传的文件。
-
登录 OSS管理控制台 。
-
在左侧导航栏,单击 Bucket 列表 , 然后单击 创建 Bucket 。
-
在 创建 Bucket 面板,选择快捷创建,按如下说明配置各项参数。
| 参数 | 示例值 |
|---|---|
| Bucket名称 | web-direct-upload |
| 地域 | 华东1(杭州) |
- 点击 完成创建 。
二、配置CORS规则
为创建的OSS Bucket配置CORS规则。
-
访问 Bucket列表 ,然后单击目标Bucket名称。
-
在 跨域设置 页面,单击 创建规则 。
-
在 创建跨域规则 面板,按以下说明设置跨域规则。
| 参数 | 示例值 |
|---|---|
| 来源 | * |
| 允许Methods | POST、PUT、GET |
| 允许Headers | * |
- 单击 确定 。
步骤二:配置服务端
一、配置用户权限
说明
为了确保部署完成后不会因为操作未授权而导致文件上传到OSS失败,建议您先按照以下步骤创建RAM用户并配置相应的权限。
在访问控制创建RAM用户
首先,创建一个调用方式为 OpenAPI调用 的RAM用户,并获取对应的访问密钥,作为业务服务器的应用程序的长期身份凭证。
-
使用云账号或账号管理员登录 RAM控制台 。
-
在左侧导航栏,选择 身份管理 > 用户 。
-
单击 创建用户 。
-
输入 登录名称 和 显示名称 。
-
在 调用方式 区域下,选择 OpenAPI调用 ,然后单击 确定 。
重要
RAM用户的AccessKey Secret只在创建时显示,后续不支持查看,请妥善保管。
单击 操作 下的 复制 ,保存调用密钥(AccessKey ID和AccessKey Secret)。
二 、在访问控制为RAM用户授予AliyunOSSFullAccess的权限
创建RAM用户后,需要授予RAM用户AliyunOSSFullAccess的权限,使其可以通过扮演RAM角色来获取管理对象存储服务(OSS)权限
-
在左侧导航栏,选择 身份管理 > 用户 。
-
在 用户 页面,找到目标RAM用户,然后单击RAM用户右侧的 添加权限 。
-
在 新增授权 页面,选择AliyunOSSFullAccess系统策略。
-
单击 确认新增授权
步骤三 .服务端获取访问凭证 & 配置Web端
您可以通过OSS控制台的PostObject Policy签名工具为通过HTML表单上传生成请求签名。通过PostObject Policy签名工具填入指定参数后,会自动生成请求签名,并校验请求签名的正确性。
- | 参数 | 是否必选 | 示例值 | 说明 | |:---:|:---:|:---:|:---:| | AccessKeyId | 是 | LTAI******** | 填写阿里云账号或RAM用户的访问密钥AccessKey,包括AccessKey ID和AccessKey Secret。 | | AccessKeySecret | 是 | KZo1******** | | 过期时间 | 是 | 2023-01-09T07:36:58.086Z | 请求过期时间,必须为GMT格式。下拉选择过期时间后会自定填充到Policy。 | | Policy | 否 | { "expiration": "2014-12-01T12:00:00.000Z", "conditions": [ {"bucket": "johnsmith" }, ["content-length-range", 1, 10], ["eq", "key", "user/eric/"], ["in", "cache-control", ["no-cache"]] ]} | PostObject请求的Policy表单域,用于验证请求的合法性。Policy为一段经过UTF-8和Base64编码的JSON文本,声明了PostObject请求必须满足的条件。
重要
对于向公共读写的Bucket执行表单上传时,Policy表单域为可选项,但强烈建议使用该域来限制PostObject请求。
关于Post Policy的更多信息,请参见 附录:Post Policy 。 |
服务端生成Post签名和Post Policy
服务端生成Post签名和Post Policy等信息的示例代码如下:
ALI_ACCESS_KEY_ID,
ALI_ACCESS_KEY_SECRET,
ALI_OSS_BUCKET,
ALI_OSS_REGION,
} = require("../config/config.default");
const OSS = require("ali-oss");
const moment = require("moment");
const path = require("path");
// 验证配置项是否存在
if (
!ALI_ACCESS_KEY_ID ||
!ALI_ACCESS_KEY_SECRET ||
!ALI_OSS_BUCKET ||
!ALI_OSS_REGION
) {
throw new Error("Missing required configuration for Ali OSS");
}
/**
* 初始化 OSS 客户端
*/
const store = new OSS({
region: ALI_OSS_REGION, // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
accessKeyId: ALI_ACCESS_KEY_ID,
accessKeySecret: ALI_ACCESS_KEY_SECRET,
bucket: ALI_OSS_BUCKET, // yourBucketName填写Bucket名称。
});
/**
* 计算 Post 签名
* @param {string} directory - 上传目录
* @param {string} name - 文件名
* @returns {Promise<Object>} - 签名结果
*/
async function calculatePostSignature(directory, name) {
const date = moment().add(1, "days");
const key = `${directory}/${name}`;
const policy = {
expiration: date.toISOString(), // 过期时间
// 条件
conditions: [
["content-length-range", 0, 5 * 1024 * 1024], // 文件大小限制
{ bucket: ALI_OSS_BUCKET }, // 存储空间
["eq", "$key", key], // 文件名 eq:等于
[
"in",
"$content-type",
["image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp"],
], // 文件类型限制 in: 包含
],
};
const formData = store.calculatePostSignature(policy);
// 获取主机
let host = await store.getBucketLocation();
// 构建 URL
let url = `http://${ALI_OSS_BUCKET}.${host.location}.aliyuncs.com`.toString();
return {
policy: formData.policy, // 策略
signature: formData.Signature, // 签名
ossAccessKeyId: formData.OSSAccessKeyId, // 访问凭证
host: url, // 主机
dir: directory, // 目录
key: key, // 文件名
};
}
注册使用
// 获取 oss 上传的 token
async getOssToken(ctx, next) {
let { filename } = ctx.request.query;
console.log(filename);
const directory = "uploads/images";
// 生成唯一文件名 (唯一id+文件扩展名)
const uniqueFileName = `${uuidv4()}.${filename.split(".")[1]}`;
const res = await calculatePostSignature(directory, uniqueFileName);
ctx.body = {
code: 0,
message: "获取oss上传token成功",
result: res,
};
}
客户端示例代码
Web端使用临时访问凭证上传文件到OSS的示例代码如下:
1.调用接口获取签名等信息
2 调用后端 返回的上传oss的地址
注意 要使用后的返回的文件名进行上传,这样可以保证唯一性,并且 政策上有检验如果不是也会上传失败
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<form>
<input type="file" name="file" id="file" />
<button type="submit">上传</button>
</form>
<body>
</body>
<script>
const form = document.querySelector("form");
const fileInput = document.querySelector("#file");
form.addEventListener("submit", (event) => {
event.preventDefault(); // 阻止表单提交的默认行为
const file = fileInput.files[0];
const filename = fileInput.files[0].name;
let type = fileInput.files[0].type;
fetch("http://localhost:9999/goods/oss/token?filename=" + filename, { method: "GET" })
.then((response) => {
if (!response.ok) {
throw new Error("获取签名失败");
}
return response.json();
})
.then((res) => {
let data = res.result;
const formData = new FormData();
formData.append("name", filename);
formData.append("policy", data.policy); // 策略
formData.append("OSSAccessKeyId", data.ossAccessKeyId);// 阿里云的key
formData.append("success_action_status", "200"); // 200 表示上传成功后,返回200状态码
formData.append("signature", data.signature); // 签名
formData.append("key", data.key); // 文件名
formData.append("file", file); // 文件
return fetch(data.host, { method: "POST", body: formData });
})
.then((response) => {
if (response.ok) {
console.log("上传成功");
alert("文件已上传");
} else {
console.log("上传失败", response);
alert("上传失败,请稍后再试");
}
})
.catch((error) => {
console.error("发生错误:", error);
});
});
</script>
</html>