服務端生成簽名以實現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
操作步驟
您可以點擊一鍵部署在您的本地服務器快速部署并體驗服務端簽名直傳。部署完成后,復制輸出頁簽下OssClientAddress的地址,通過您的瀏覽器體驗文件直傳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
*
單擊確定。
步驟二:配置服務端
在實際部署時,如果您已經有自己的業務服務器,則無需進行準備工作,直接跳轉到一、配置用戶權限。
準備工作:創建一臺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退訂說明》,單擊確認下單。在提示成功的對話框中,單擊管理控制臺,即可在控制臺查看到創建好的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');