服务端生成签名以实现Web端直传,使用户能够通过Web浏览器直接使用PostObject接口上传文件到OSS。此过程通过签名机制确保上传的安全性。同时,根据具体业务需求,您可以在服务端生成上传策略(Policy),以限制上传操作。
方案概览
服务端生成签名实现Web端直传的过程如下:
要实现服务端签名直传,只需3步:
由于使用了临时访问凭证,整个过程中不会泄露业务服务器的长期密钥,保证了文件上传的安全性。
配置OSS:配置OSS,在控制台创建一个Bucket,用于存储用户上传的文件。同时,为 Bucket 配置跨域资源共享(CORS) 规则,以允许来自服务端的跨域请求。
配置服务端:配置服务端,调用STS服务获取一个临时访问凭证,然后使用临时访问凭证和服务端预设的上传策略(如Bucket名称、目录路径、过期时间等)生成签名授权用户在一定时间内进行文件上传。
配置Web端:配置Web端,构造HTML表单请求,通过表单提交使用签名将文件上传到OSS。
示例工程
Java完整示例工程请参见:server-signed-direct-upload-java.zip
Python完整示例工程请参见:server-signed-direct-upload-python.zip
操作步骤
步骤一:配置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
*
单击确定。
步骤二:配置服务端
在实际部署时,如果您已经有自己的业务服务器,则无需进行准备工作,直接跳转到一、配置用户权限。
准备工作:创建一台ECS实例作为业务服务器
操作一:创建ECS实例
请您进入自定义购买页面,并根据如下各模块的内容,创建或选择购买ECS实例所需的基础资源。
选择地域 & 付费类型
根据业务需求,选择合适的付费类型。本文选择按量付费模式,此模式操作相对灵活。
基于业务场景对时延的要求,选择地域。通常来说离ECS实例的物理距离越近,网络时延越低,访问速度越快。本文以选择华东1(杭州)为例。
创建专有网络VPC & 交换机
创建VPC时,请您选择和ECS相同的地域,并根据业务需求规划网段。本文以创建华东1(杭州)地域的VPC和交换机为例。创建完毕后返回ECS购买页,刷新并选择VPC及交换机。
说明创建VPC时,可同时创建交换机。
选择规格 & 镜像
选择实例的规格及镜像,镜像为实例确定安装的操作系统及版本。本文选择的实例规格为
ecs.e-c1m1.large
,在满足测试需求的同时,价格较为实惠。镜像为公共镜像Alibaba Cloud Linux 3.2104 LTS 64位
。
选择存储
为ECS实例选择系统盘,并按需选择数据盘。本文实现简单Web系统搭建,只需要系统盘存储操作系统,无需数据盘。
绑定公网IP
本实例需要支持公网访问。为了简化操作,本文选择直接为实例分配公网IP。您也可以在创建实例后,为实例绑定弹性公网IP,具体操作,请参见将EIP绑定至ECS实例。
说明若未绑定公网IP,将无法使用SSH或RDP通过公网直接访问实例,也无法通过公网验证实例中Web服务的搭建。
本文选择按使用流量的带宽计费模式。此模式只需为所消耗的公网流量付费。更多信息,请参见公网带宽计费。
创建安全组
为实例创建安全组。安全组是一种虚拟网络防火墙,能够控制ECS实例的出入流量。创建时,需要设置放行以下指定端口,便于后续访问ECS实例。
端口范围:SSH(22)、RDP(3389)、HTTP(80)、HTTPS(443)。
说明端口范围处选中的是ECS实例上运行的应用需开放的端口。
此处创建的安全组默认设置0.0.0.0/0作为源的规则。0.0.0.0/0表示允许全网段设备访问指定的端口,如果您知道请求端的IP地址,建议后续设置为具体的IP范围。具体操作,请参见修改安全组规则。
创建密钥对
密钥对可作为登录时证明个人身份的安全凭证,创建完成后,必须下载私钥,以供后续连接ECS实例时使用。创建完毕后返回ECS购买页,刷新并选择密钥对。
root
具有操作系统的最高权限,使用root
作为登录名可能会导致安全风险,建议您选择ecs-user
作为登录名。说明创建密钥对后,私钥会自动下载,请您关注浏览器的下载记录,保存
.pem
格式的私钥文件。
创建并查看ECS实例
创建或选择好ECS实例所需的基础资源后,勾选《云服务器ECS服务条款》、《通用服务条款》,单击确认下单。在提示成功的对话框中,单击管理控制台,即可在控制台查看到创建好的ECS实例。请您保存以下数据,以便在后续操作中使用。
实例ID:便于在实例列表中查询到该实例。
地域:便于在实例列表中查询到该实例。
公网IP地址:便于在后续使用ECS实例时,做Web服务的部署结果验证。
操作二:连接ECS实例
在云服务器ECS控制台的实例列表页面,根据地域、实例ID找到创建好的ECS实例,单击操作列下的远程连接。
在远程连接对话框中,单击通过Workbench远程连接对应的立即登录。
在登录实例对话框中,选择认证方式为SSH密钥认证,用户名为
ecs-user
,输入或上传创建密钥对时下载的私钥文件,单击确定,即可登录ECS实例。说明私钥文件在创建密钥对时自动下载到本地,请您关注浏览器的下载记录,查找
.pem
格式的私钥文件。显示如下页面后,即说明您已成功登录ECS实例。
一、配置用户权限
为了确保部署完成后不会因为操作未授权而导致文件上传到OSS失败,建议您先按照以下步骤创建RAM用户并配置相应的权限。
操作一:在访问控制创建RAM用户
首先,创建一个RAM用户,并获取对应的访问密钥,作为业务服务器的应用程序的长期身份凭证。
使用云账号或账号管理员登录RAM控制台。
在左侧导航栏,选择身份管理 > 用户。
单击创建用户。
输入登录名称和显示名称。
在调用方式区域下,选择使用永久 AccessKey 访问,然后单击确定。
RAM用户的AccessKey Secret只在创建时显示,后续不支持查看,请妥善保管。
单击操作下的复制,保存调用密钥(AccessKey ID和AccessKey Secret)。
操作二:在访问控制为RAM用户授予调用AssumeRole接口的权限
创建RAM用户后,需要授予RAM用户调用STS服务的AssumeRole接口的权限,使其可以通过扮演RAM角色来获取临时身份凭证。
在左侧导航栏,选择身份管理 > 用户。
在用户页面,找到目标RAM用户,然后单击RAM用户右侧的添加权限。
在新增授权页面,选择AliyunSTSAssumeRoleAccess系统策略。
说明授予RAM用户调用STS服务AssumeRole接口的固定权限是AliyunSTSAssumeRoleAccess,与后续获取临时访问凭证以及通过临时访问凭证发起OSS请求所需权限无关。
单击确认新增授权。
操作三:在访问控制创建RAM角色
为当前云账号创建一个RAM角色,并获取对应的角色的ARN(Aliyun Resource Name,阿里云资源名称),用于RAM用户之后进行扮演。
在左侧导航栏,选择身份管理 > 角色。
单击创建角色,可信实体类型选择阿里云账号,单击下一步。
填写角色名称,选择当前云账号。
单击完成。完成角色创建后,单击关闭。
在RAM角色管理页面,搜索框输入角色名称,例如
oss-web-upload
。单击复制,保存角色的ARN。
操作四:在访问控制创建上传文件的权限策略
按照最小授权原则,为RAM角色创建一个自定义权限策略,限制只能向指定OSS的存储空间进行上传操作。
在左侧导航栏,选择权限管理 > 权限策略。
单击创建权限策略。
在创建权限策略页面,单击脚本编辑,将以下脚本中的
<Bucket名称>
替换为准备工作中创建的Bucket名称web-direct-upload
。{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": "oss:PutObject", "Resource": "acs:oss:*:*:<Bucket名称>/*" } ] }
策略配置完成后,单击继续编辑基本信息。
在基本信息区域,填写策略名称,然后单击确定。
操作五:在访问控制为RAM角色授予权限
为RAM角色授予创建的自定义权限,以便该RAM角色被扮演时能获取所需的权限。
在左侧导航栏,选择身份管理 > 角色。
在角色页面,找到目标RAM角色,然后单击RAM角色右侧的新增授权。
在新增授权页面下的自定义策略页签,选择已创建的自定义权限策略。
单击确定。
二、服务端获取临时访问凭证并计算签名
您可以参考以下代码,在服务端进行POST签名版本4(推荐)的计算工作。关于policy表单域详细配置信息,请参见policy表单域。
Java
在Maven项目中,导入以下依赖。
<!-- https://mvnrepository.com/artifact/com.aliyun/credentials-java -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>credentials-java</artifactId>
<version>0.3.4</version>
</dependency>
<dependency>
<groupId>com.aliyun.kms</groupId>
<artifactId>kms-transfer-client</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>sts20150401</artifactId>
<version>1.1.6</version>
</dependency>
您可以参考如下代码来完成服务端获取临时访问凭证并计算POST签名:
package com.sanfeng.oss.serversigneddirectupload.demos.web;
import com.aliyun.sts20150401.models.AssumeRoleResponse;
import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import com.aliyun.tea.TeaException;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.binary.Base64;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Controller
public class WebController {
//OSS基础信息 替换为实际的 bucket 名称和 region-id
String bucket = "web-direct-upload";
String region = "cn-hangzhou";
String host = "http://" + bucket + ".oss-" + region + ".aliyuncs.com";
//指定上传到OSS的文件前缀。
String upload_dir = "dir";
//指定过期时间,单位为秒。
Long expire_time = 3600L;
/**
* 通过指定有效的时长(秒)生成过期时间。
* @param seconds 有效时长(秒)。
* @return ISO8601 时间字符串,如:"2014-12-01T12:00:00.000Z"。
*/
public static String generateExpiration(long seconds) {
// 获取当前时间戳(以秒为单位)
long now = Instant.now().getEpochSecond();
// 计算过期时间的时间戳
long expirationTime = now + seconds;
// 将时间戳转换为Instant对象,并格式化为ISO8601格式
Instant instant = Instant.ofEpochSecond(expirationTime);
// 定义时区
ZoneId zone = ZoneId.systemDefault(); // 使用系统默认时区
// 将 Instant 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = instant.atZone(zone);
// 定义日期时间格式,例如2023-12-03T13:00:00.000Z
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
// 格式化日期时间
String formattedDate = zonedDateTime.format(formatter);
// 输出结果
return formattedDate;
}
//初始化STS Client
public static com.aliyun.sts20150401.Client createStsClient() throws Exception {
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
// 建议使用更安全的 STS 方式。
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
// 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_ID。
.setAccessKeyId(System.getenv("OSS_ACCESS_KEY_ID"))
// 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_SECRET。
.setAccessKeySecret(System.getenv("OSS_ACCESS_KEY_SECRET"));
// Endpoint 请参考 https://api.aliyun.com/product/Sts
config.endpoint = "sts.cn-hangzhou.aliyuncs.com";
return new com.aliyun.sts20150401.Client(config);
}
//获取STS临时凭证
public static AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials getCredential() throws Exception {
com.aliyun.sts20150401.Client client = WebController.createStsClient();
com.aliyun.sts20150401.models.AssumeRoleRequest assumeRoleRequest = new com.aliyun.sts20150401.models.AssumeRoleRequest()
.setRoleArn(System.getenv("OSS_ACCESS_KEY_ID"))
.setRoleSessionName("yourRoleSessionName");// 自定义会话名称
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
AssumeRoleResponse response = client.assumeRoleWithOptions(assumeRoleRequest, runtime);
// credentials里包含了后续要用到的AccessKeyId、AccessKeySecret和SecurityToken。
return response.body.credentials;
} catch (TeaException error) {
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
System.out.println(error.getMessage());
// 诊断地址
System.out.println(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
System.out.println(error.getMessage());
// 诊断地址
System.out.println(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
}
return null;
}
@GetMapping("/get_post_signature_for_oss_upload")
public ResponseEntity<Map<String, String>> getPostSignatureForOssUpload() throws Exception {
AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials sts_data = getCredential();
String accesskeyid = sts_data.accessKeyId;
String accesskeysecret = sts_data.accessKeySecret;
String securitytoken = sts_data.securityToken;
System.out.println("sts accesskeyid:"+accesskeyid);
System.out.println("sts accesskeysecret:"+accesskeysecret);
System.out.println("sts securitytoken:"+securitytoken);
//获取x-oss-credential里的date,当前日期,格式为yyyyMMdd
LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String date = today.format(formatter);
//获取x-oss-date
ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC);
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
String x_oss_date = now.format(formatter2);
// 步骤1:创建policy。
String x_oss_credential = accesskeyid + "/" + date + "/" + region + "/oss/aliyun_v4_request";
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> policy = new HashMap<>();
policy.put("expiration", generateExpiration(expire_time));
List<Object> conditions = new ArrayList<>();
Map<String, String> bucketCondition = new HashMap<>();
bucketCondition.put("bucket", bucket);
conditions.add(bucketCondition);
Map<String, String> securityTokenCondition = new HashMap<>();
securityTokenCondition.put("x-oss-security-token", securitytoken);
conditions.add(securityTokenCondition);
Map<String, String> signatureVersionCondition = new HashMap<>();
signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256");
conditions.add(signatureVersionCondition);
Map<String, String> credentialCondition = new HashMap<>();
credentialCondition.put("x-oss-credential", x_oss_credential); // 替换为实际的 access key id
conditions.add(credentialCondition);
Map<String, String> dateCondition = new HashMap<>();
dateCondition.put("x-oss-date", x_oss_date);
conditions.add(dateCondition);
conditions.add(Arrays.asList("content-length-range", 1, 10240000));
conditions.add(Arrays.asList("eq", "$success_action_status", "200"));
conditions.add(Arrays.asList("starts-with", "$key", upload_dir));
policy.put("conditions", conditions);
String jsonPolicy = mapper.writeValueAsString(policy);
// 步骤2:构造待签名字符串(StringToSign)。
String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
// System.out.println("stringToSign: " + stringToSign);
// 步骤3:计算SigningKey。
byte[] dateKey = hmacsha256(("aliyun_v4" + accesskeysecret).getBytes(), date);
byte[] dateRegionKey = hmacsha256(dateKey, region);
byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
// System.out.println("signingKey: " + BinaryUtil.toBase64String(signingKey));
// 步骤4:计算Signature。
byte[] result = hmacsha256(signingKey, stringToSign);
String signature = BinaryUtil.toHex(result);
// System.out.println("signature:" + signature);
Map<String, String> response = new HashMap<>();
// 将数据添加到 map 中
response.put("version", "OSS4-HMAC-SHA256");
// 这里是易错点,不能直接传policy,需要做一下Base64编码
response.put("policy", stringToSign);
response.put("credential", x_oss_credential);
response.put("ossdate", x_oss_date);
response.put("signature", signature);
response.put("token", securitytoken);
response.put("dir", upload_dir);
response.put("host", host);
// 返回带有状态码 200 (OK) 的 ResponseEntity,返回给Web端,进行PostObject操作
return ResponseEntity.ok(response);
}
public static byte[] hmacsha256(byte[] key, String data) {
try {
// 初始化HMAC密钥规格,指定算法为HMAC-SHA256并使用提供的密钥。
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
// 获取Mac实例,并通过getInstance方法指定使用HMAC-SHA256算法。
Mac mac = Mac.getInstance("HmacSHA256");
// 使用密钥初始化Mac对象。
mac.init(secretKeySpec);
// 执行HMAC计算,通过doFinal方法接收需要计算的数据并返回计算结果的数组。
byte[] hmacBytes = mac.doFinal(data.getBytes());
return hmacBytes;
} catch (Exception e) {
throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
}
}
}
Python
执行以下命令安装依赖。
pip install flask pip install alibabacloud_tea_openapi alibabacloud_sts20150401 alibabacloud_credentials
请参考如下代码来完成Python服务端获取临时访问凭证STStoken并构建上传策略以计算POST签名。
from flask import Flask, render_template, jsonify from alibabacloud_tea_openapi.models import Config from alibabacloud_sts20150401.client import Client as Sts20150401Client from alibabacloud_sts20150401 import models as sts_20150401_models from alibabacloud_credentials.client import Client as CredentialClient import os import json import base64 import hmac import datetime import time import hashlib app = Flask(__name__) # 配置环境变量 OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_ID, OSS_STS_ROLE_ARN。 access_key_id = os.environ.get('OSS_ACCESS_KEY_ID') access_key_secret = os.environ.get('OSS_ACCESS_KEY_SECRET') role_arn_for_oss_upload = os.environ.get('OSS_STS_ROLE_ARN') # 自定义会话名称 role_session_name = 'role_session_name' # 替换为实际的bucket名称和region_id bucket = 'examplebucket' region_id = 'cn-hangzhou' host = f'http://{bucket}.oss-cn-hangzhou.aliyuncs.com' # 指定过期时间,单位为秒 expire_time = 3600 # 指定上传到OSS的文件前缀。 upload_dir = 'dir' def hmacsha256(key, data): """ 计算HMAC-SHA256哈希值的函数 :param key: 用于计算哈希的密钥,字节类型 :param data: 要进行哈希计算的数据,字符串类型 :return: 计算得到的HMAC-SHA256哈希值,字节类型 """ try: mac = hmac.new(key, data.encode(), hashlib.sha256) hmacBytes = mac.digest() return hmacBytes except Exception as e: raise RuntimeError(f"Failed to calculate HMAC-SHA256 due to {e}") @app.route("/") def hello_world(): return render_template('index.html') @app.route('/get_post_signature_for_oss_upload', methods=['GET']) def generate_upload_params(): # 初始化配置,直接传递凭据 config = Config( region_id=region_id, access_key_id=access_key_id, access_key_secret=access_key_secret ) # 创建 STS 客户端并获取临时凭证 sts_client = Sts20150401Client(config=config) assume_role_request = sts_20150401_models.AssumeRoleRequest( role_arn=role_arn_for_oss_upload, role_session_name=role_session_name ) response = sts_client.assume_role(assume_role_request) token_data = response.body.credentials.to_map() # 使用 STS 返回的临时凭据 temp_access_key_id = token_data['AccessKeyId'] temp_access_key_secret = token_data['AccessKeySecret'] security_token = token_data['SecurityToken'] now = int(time.time()) # 将时间戳转换为datetime对象 dt_obj = datetime.datetime.utcfromtimestamp(now) # 在当前时间增加3小时,设置为请求的过期时间 dt_obj_plus_3h = dt_obj + datetime.timedelta(hours=3) # 请求时间 dt_obj_1 = dt_obj.strftime('%Y%m%dT%H%M%S') + 'Z' # 请求日期 dt_obj_2 = dt_obj.strftime('%Y%m%d') # 请求过期时间 expiration_time = dt_obj_plus_3h.strftime('%Y-%m-%dT%H:%M:%S.000Z') # 构建 Policy 并生成签名 policy = { "expiration": expiration_time, "conditions": [ ["eq", "$success_action_status", "200"], {"x-oss-signature-version": "OSS4-HMAC-SHA256"}, {"x-oss-credential": f"{temp_access_key_id}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request"}, {"x-oss-security-token": security_token}, {"x-oss-date": dt_obj_1}, ] } print(dt_obj_1) policy_str = json.dumps(policy).strip() # 步骤2:构造待签名字符串(StringToSign) stringToSign = base64.b64encode(policy_str.encode()).decode() # 步骤3:计算SigningKey dateKey = hmacsha256(("aliyun_v4" + temp_access_key_secret).encode(), dt_obj_2) dateRegionKey = hmacsha256(dateKey, "cn-hangzhou") dateRegionServiceKey = hmacsha256(dateRegionKey, "oss") signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request") # 步骤4:计算Signature result = hmacsha256(signingKey, stringToSign) signature = result.hex() # 组织返回数据 response_data = { 'policy': stringToSign, #表单域 'x_oss_signature_version': "OSS4-HMAC-SHA256", #指定签名的版本和算法,固定值为OSS4-HMAC-SHA256 'x_oss_credential': f"{temp_access_key_id}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request", #指明派生密钥的参数集 'x_oss_date': dt_obj_1, #请求的时间 'signature': signature, #签名认证描述信息 'host': host, 'dir': upload_dir, 'security_token': security_token #安全令牌 } return jsonify(response_data) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)
步骤三:配置Web端
Web端构造并提交表单上传请求
当Web端从服务端接收到所有必需的信息后,就可以构建HTML表单请求了。此请求将直接与OSS服务进行通信,从而实现文件的上传。
Web端接收到的响应示例
业务服务器向Web端返回STS Token和上传策略。
{
"dir": "user-dirs",
"host": "http://examplebucket.oss-cn-hangzhou.aliyuncs.com",
"policy": "eyJl****",
"security_token": "CAIS****",
"signature": "9103****",
"x_oss_credential": "STS.NSpW****/20241127/cn-hangzhou/oss/aliyun_v4_request",
"x_oss_date": "20241127T060941Z",
"x_oss_signature_version": "OSS4-HMAC-SHA256"
}
Body中的各字段说明如下:
字段 | 描述 |
dir | 限制上传的文件前缀。 |
host | Bucket域名。 |
policy | 用户表单上传的策略(Policy),详情请参见Post Policy。 |
security_token | 安全令牌。 |
signature | 对Policy签名后的字符串。详情请参见Post Signature。 |
x_oss_credential | 指明派生密钥的参数集。 |
x_oss_date | 请求的时间,其格式遵循ISO 8601日期和时间标准,例如 |
x_oss_signature_version | 指定签名的版本和算法,固定值为OSS4-HMAC-SHA256 |
表单请求中包含文件内容和服务器返回的参数。
通过这个请求,Web端可以直接与阿里云的OSS进行通信,完成文件上传。
除file表单域外,包含key在内的其他所有表单域的大小均不能超过8 KB。
Web端上传默认同名覆盖,如果您不希望覆盖同名文件,可以在上传请求的header中携带参数x-oss-forbid-overwrite,并指定其值为true。当您上传的文件在OSS中存在同名文件时,该文件会上传失败,并返回FileAlreadyExists错误。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>服务端生成签名上传文件到OSS</title>
</head>
<body>
<div class="container">
<form>
<div class="mb-3">
<label for="file" class="form-label">选择文件:</label>
<input type="file" class="form-control" id="file" name="file" required />
</div>
<button type="submit" class="btn btn-primary">上传</button>
</form>
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector("form");
const fileInput = document.querySelector("#file");
form.addEventListener("submit", (event) => {
event.preventDefault();
const file = fileInput.files[0];
if (!file) {
alert('请选择一个文件再上传。');
return;
}
const filename = file.name;
fetch("/get_post_signature_for_oss_upload", { method: "GET" })
.then((response) => {
if (!response.ok) {
throw new Error("获取签名失败");
}
return response.json();
})
.then((data) => {
let formData = new FormData();
formData.append("success_action_status", "200");
formData.append("policy", data.policy);
formData.append("x-oss-signature", data.signature);
formData.append("x-oss-signature-version", "OSS4-HMAC-SHA256");
formData.append("x-oss-credential", data.x_oss_credential);
formData.append("x-oss-date", data.x_oss_date);
formData.append("key", data.dir + file.name); // 文件名
formData.append("x-oss-security-token", data.security_token);
formData.append("file", 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>
</body>
</html>
HTML表单包含一个文件输入框和一个提交按钮,用户可以选择要上传的文件并提交表单。
当表单提交时,JavaScript代码会阻止默认的表单提交行为,然后通过AJAX请求从服务器获取上传所需的签名信息。
获取到签名信息后,构造一个
FormData
对象,包含所有必要的表单字段。通过
fetch
方法发送POST请求到OSS服务的URL,完成文件上传。
如果上传成功,显示“文件已上传”的提示;如果上传失败,显示相应的错误信息。
结果验证
以上步骤部署完成后,您可以访问服务器地址,体验Web端签名直传功能。
通过浏览器访问服务端地址,然后点击上传按钮选择上传的文件。效果示例如下:
在Bucket列表页面,选择您之前创建的用来存放用户上传文件的Bucket并打开,您可以在上传列表中看到您通过Web端上传的文件。
建议配置
将敏感信息配置为环境变量
建议您把敏感信息(如accessKeyId
、accessKeySecret
和roleArn
)配置到环境变量,从而避免在代码里显式地配置,降低泄露风险。
您可以仅在当前会话中使用该环境变量,可以参照以下步骤添加临时环境变量。
Linux系统
执行以下命令。
export OSS_ACCESS_KEY_ID="your-access-key-id" export OSS_ACCESS_KEY_SECRET="your-access-key-secret" export OSS_STS_ROLE_ARN="your-role-arn"
执行以下命令,验证该环境变量是否生效。
echo $OSS_ACCESS_KEY_ID echo $OSS_ACCESS_KEY_SECRET echo $OSS_STS_ROLE_ARN
macOS系统
执行以下命令。
export OSS_ACCESS_KEY_ID="your-access-key-id" export OSS_ACCESS_KEY_SECRET="your-access-key-secret" export OSS_STS_ROLE_ARN="your-role-arn"
执行以下命令,验证该环境变量是否生效。
echo $OSS_ACCESS_KEY_ID echo $OSS_ACCESS_KEY_SECRET echo $OSS_STS_ROLE_ARN
Windows系统
在CMD中运行以下命令。
set OSS_ACCESS_KEY_ID "your-access-key-id" set OSS_ACCESS_KEY_SECRET "your-access-key-secret" set OSS_STS_ROLE_ARN "your-role-arn"
打开一个新的CMD窗口。
在新的CMD窗口运行以下命令,检查环境变量是否生效。
echo $OSS_ACCESS_KEY_ID echo $OSS_ACCESS_KEY_SECRET echo $OSS_STS_ROLE_ARN
服务端签名直传并设置上传回调
如果您需要获取更多关于用户上传文件的信息,例如文件名称、图片大小等,请使用上传回调方案。通过设置上传回调,您可以在用户上传文件后,自动接收到相关文件信息。关于如何配置服务端签名直传并设置上传回调,请参见服务器端签名直传并设置上传回调。
配置CORS规则时将来源设为服务器地址
在之前的操作步骤中,为了简化流程,将允许的跨域请求来源设置为通配符 *
。然而,出于安全考虑,建议您对来源进行更严格的限制。为此,您可以将创建的OSS Bucket的跨域资源共享来源参数设置为您业务服务器的具体地址。这样一来,唯有来自您指定服务器的请求才能被授权执行跨域操作,有效增强了系统的安全性。
参数 | 示例值 |
来源 |
|
允许Methods | POST、PUT、GET |
允许Headers | * |
清理资源
在本方案中,您创建了1台ECS实例、1个OSS Bucket、1个RAM用户和1个RAM角色。测试完方案后,您可以参考以下规则处理对应产品的资源,避免继续产生费用或产生安全风险。
释放ECS实例
如果您不再需要这台实例,可以将其释放。释放后,实例停止计费,数据不可恢复。具体操作如下:
返回云服务器ECS控制台的实例列表页面,根据地域、实例ID找到目标ECS实例,单击操作列下的。
选择释放。
确认实例无误后,选择立即释放,单击下一步。
确认即将释放的关联资源,并了解相关数据风险后,单击确认,即可完成ECS实例的释放。
系统盘、分配的公网IP将随实例释放。
安全组、交换机和VPC不会随实例释放,但它们均为免费资源,您可根据实际的业务需要选择性删除。
弹性公网IP不会随实例释放,且不是免费资源,您可根据实际的业务需要选择性删除。
删除Bucket
登录OSS管理控制台。
单击Bucket 列表,然后单击目标Bucket名称。
删除Bucket的所有文件(Object)。
在左侧导航栏,单击删除Bucket,然后按照页面指引完成删除操作。
删除RAM用户
使用RAM管理员登录RAM控制台。
在左侧导航栏,选择身份管理 > 用户。
在用户页面,单击目标RAM用户操作列的删除。
您也可以选中多个RAM用户,然后单击用户列表下方的删除用户,批量将多个RAM用户移入回收站。
在删除用户对话框,仔细阅读删除影响,然后输入目标RAM用户名称,最后单击移入回收站。
删除RAM角色
使用RAM管理员登录RAM控制台。
在左侧导航栏,选择身份管理 > 角色。
在角色页面,单击目标RAM角色操作列的删除角色。
在删除角色对话框,输入RAM角色名称,然后单击删除角色。
说明如果RAM角色被授予了权限策略,删除角色时,会同时解除授权。
常见问题
是否可以支持分片上传大文件、断点续传?
此方案是使用HTML表单上传的方式上传文件,不支持基于分片上传大文件和基于分片断点续传的场景。如果您想实现分片上传大文件或断点续传,请参考服务端生成STS临时访问凭证。
如何防止上传的文件被覆盖
如果希望防止文件覆盖,可以在上传请求的Header中添加x-oss-forbid-overwrite
参数,并将其值设为true
。例如:
formData.append('x-oss-forbid-overwrite', 'true');