全部產品
Search
文件中心

Object Storage Service:服務端簽名直傳

更新時間:Mar 28, 2025

您可以使用PostObject介面,將檔案直接從 Web 端上傳到 OSS,伺服器產生的簽名為直傳操作提供安全保障,同時支援配置上傳策略(Policy)以限制上傳操作並滿足業務需求。

方案概覽

服務端產生簽名實現Web端直傳的過程如下:

要實現服務端簽名直傳,只需3步:

說明

由於使用了臨時訪問憑證,整個過程中不會泄露商務服務器的長期密鑰,保證了檔案上傳的安全性。

  1. 配置OSS:配置OSS,在控制台建立一個Bucket,用於儲存使用者上傳的檔案。同時,為 Bucket 配置跨域資源共用(CORS) 規則,以允許來自服務端的跨域請求。

  2. 佈建服務端:佈建服務端,調用STS服務擷取一個臨時訪問憑證,然後使用臨時訪問憑證和服務端預設的上傳策略(如Bucket名稱、目錄路徑、到期時間等)產生簽名授權使用者在一定時間內進行檔案上傳。

  3. 配置Web端:配置Web端,構造HTML表單請求,通過表單提交使用簽名將檔案上傳到OSS。

樣本工程

操作步驟

步驟一:配置OSS

一、建立Bucket

建立一個OSS Bucket,用於儲存Web應用在瀏覽器環境中直接上傳的檔案。

  1. 登入OSS管理主控台

  2. 在左側導覽列,單擊Bucket 列表然後單擊建立 Bucket

  3. 建立 Bucket面板,選擇快捷建立,按如下說明配置各項參數。

    參數

    樣本值

    Bucket名稱

    web-direct-upload

    地域

    華東1(杭州)

  4. 點擊完成建立

二、配置CORS規則

為建立的OSS Bucket配置CORS規則。

  1. 訪問Bucket列表,然後單擊目標Bucket名稱。

  2. 跨域設置頁面,單擊創建規則

  3. 跨域規則面板,按以下說明設定跨域規則。

    參數

    樣本值

    來源

    *

    允許Methods

    POST、PUT、GET

    允許Headers

    *

  1. 單擊確定

步驟二:佈建服務端

說明

在實際部署時,如果您已經有自己的商務服務器,則無需進行準備工作,直接跳轉到一、配置使用者權限

準備工作:建立一台ECS執行個體作為商務服務器

操作一:建立ECS執行個體

請您進入自訂購買頁面,並根據如下各模組的內容,建立或選擇購買ECS執行個體所需的基礎資源。

  1. 選擇地區 & 付費類型

    1. 根據業務需求,選擇合適的付費類型。本文選擇隨用隨付模式,此模式操作相對靈活。

    2. 基於業務情境對時延的要求,選擇地區。通常來說離ECS執行個體的物理距離越近,網路時延越低,訪問速度越快。本文以選擇華東1(杭州)為例。

      image

  1. 建立Virtual Private Cloud & 交換器

    建立VPC時,請您選擇和ECS相同的地區,並根據業務需求規劃網段。本文以建立華東1(杭州)地區的VPC和交換器為例。建立完畢後返回ECS購買頁,重新整理並選擇VPC及交換器。

    說明

    建立VPC時,可同時建立交換器。

    image

    image

    image

  1. 選擇規格 & 鏡像

    選擇執行個體的規格及鏡像,鏡像為執行個體確定安裝的作業系統及版本。本文選擇的執行個體規格為ecs.e-c1m1.large,在滿足測試需求的同時,價格較為實惠。鏡像為公用鏡像Alibaba Cloud Linux 3.2104 LTS 64位

    image

  1. 選擇儲存

    為ECS執行個體選擇系統硬碟,並按需選擇資料盤。本文實現簡單Web系統搭建,只需要系統硬碟儲存作業系統,無需資料盤。

    image

  1. 綁定公網IP

    本執行個體需要支援公網訪問。為了簡化操作,本文選擇直接為執行個體分配公網IP。您也可以在建立執行個體後,為執行個體綁定Elastic IP Address,具體操作,請參見將EIP綁定至ECS執行個體

    說明
    • 若未綁定公網IP,將無法使用SSH或RDP通過公網直接存取執行個體,也無法通過公網驗證執行個體中Web服務的搭建。

    • 本文選擇按使用流量的頻寬計費模式。此模式只需為所消耗的公網流量付費。更多資訊,請參見公網頻寬計費

    image

  1. 建立安全性群組

    為執行個體建立安全性群組。安全性群組是一種虛擬網路防火牆,能夠控制ECS執行個體的出入流量。建立時,需要設定允許存取以下指定連接埠,便於後續訪問ECS執行個體。

    連接埠範圍:SSH(22)、RDP(3389)、HTTP(80)、HTTPS(443)。

    說明
    • 連接埠範圍處選中的是ECS執行個體上啟動並執行應用需開放的連接埠。

    • 此處建立的安全性群組預設設定0.0.0.0/0作為源的規則。0.0.0.0/0表示允許全網段裝置訪問指定的連接埠,如果您知道請求端的IP地址,建議後續設定為具體的IP範圍。具體操作,請參見修改安全性群組規則

    image

  1. 建立金鑰組

    1. 金鑰組可作為登入時證明個人身份的安全憑證,建立完成後,必須下載私密金鑰,以供後續串連ECS執行個體時使用。建立完畢後返回ECS購買頁,重新整理並選擇金鑰組。

    2. root具有作業系統的最高許可權,使用root作為登入名稱可能會導致安全風險,建議您選擇ecs-user作為登入名稱。

      說明

      建立金鑰組後,私密金鑰會自動下載,請您關注瀏覽器的下載記錄,儲存.pem格式的私密金鑰檔案。

      image

  1. 建立並查看ECS執行個體

    建立或選擇好ECS執行個體所需的基礎資源後,單擊確認下單。在提示成功的對話方塊中,單擊管理主控台,即可在控制台查看到建立好的ECS執行個體。請您儲存以下資料,以便在後續操作中使用。

    • 執行個體ID:便於在執行個體列表中查詢到該執行個體。

    • 地區:便於在執行個體列表中查詢到該執行個體。

    • 公網IP地址:便於在後續使用ECS執行個體時,做Web服務的部署結果驗證。

    imageimage

操作二:串連ECS執行個體

  1. Elastic Compute Service控制台執行個體列表頁面,根據地區、執行個體ID找到建立好的ECS執行個體,單擊操作列下的遠端連線image

  2. 遠端連線對話方塊中,單擊通過Workbench遠端連線對應的立即登入image

  3. 登入執行個體對話方塊中,選擇認證方式SSH密鑰認證,使用者名稱為ecs-user,輸入或上傳建立金鑰組時下載的私密金鑰檔案,單擊確定,即可登入ECS執行個體。

    說明

    私密金鑰檔案在建立金鑰組時自動下載到本地,請您關注瀏覽器的下載記錄,尋找.pem格式的私密金鑰檔案。

    image

  4. 顯示如下頁面後,即說明您已成功登入ECS執行個體。image

一、配置使用者權限

說明

為了確保部署完成後不會因為操作未授權而導致檔案上傳到OSS失敗,建議您先按照以下步驟建立RAM使用者並配置相應的許可權。

操作一:在存取控制建立RAM使用者

首先,建立一個RAM使用者,並擷取對應的存取金鑰,作為商務服務器的應用程式的長期身份憑證。

  1. 使用雲帳號或帳號管理員登入RAM控制台

  2. 在左側導覽列,選擇身份管理 > 使用者

  3. 單擊建立使用者

  4. 輸入登入名稱稱顯示名稱

  5. 訪問方式地區下,選擇使用永久 AccessKey 訪問,然後單擊確定

重要

RAM使用者的AccessKey Secret只在建立時顯示,後續不支援查看,請妥善保管。

  1. 單擊操作下的複製,儲存調用密鑰(AccessKey ID和AccessKey Secret)。

操作二:在存取控制為RAM使用者授予調用AssumeRole介面的許可權

建立RAM使用者後,需要授予RAM使用者調用STS服務的AssumeRole介面的許可權,使其可以通過扮演RAM角色來擷取臨時身份憑證。

  1. 在左側導覽列,選擇身份管理 > 使用者

  2. 使用者頁面,找到目標RAM使用者,然後單擊RAM使用者右側的添加許可權

  3. 新增授權頁面,選擇AliyunSTSAssumeRoleAccess系統策略。

    說明

    授予RAM使用者調用STS服務AssumeRole介面的固定許可權是AliyunSTSAssumeRoleAccess,與後續擷取臨時訪問憑證以及通過臨時訪問憑證發起OSS請求要求的權限無關。

  4. 單擊確認新增授權

操作三:在存取控制建立RAM角色

為當前雲帳號建立一個RAM角色,並擷取對應的角色的ARN(Aliyun Resource Name,阿里雲資源名稱),用於RAM使用者之後進行扮演。

  1. 在左側導覽列,選擇身份管理 > 角色

  2. 單擊建立角色,可信實體類型選擇雲帳號

  3. 選擇當前雲帳號,單擊確定

  4. 填寫角色名稱,單擊確定

  5. 在RAM角色管理頁面,單擊複製,儲存角色的ARN。

操作四:在存取控制建立上傳檔案的權限原則

按照最小授權原則,為RAM角色建立一個自訂權限原則,限制只能向指定OSS的儲存空間進行上傳操作。

  1. 在左側導覽列,選擇許可權管理 > 權限原則

  2. 單擊建立權限原則

  3. 建立權限原則頁面,單擊指令碼編輯,將以下指令碼中的<Bucket名稱>替換為準備工作中建立的Bucket名稱web-direct-upload

    {
      "Version": "1",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "oss:PutObject",
          "Resource": "acs:oss:*:*:<Bucket名稱>/*"
        }
      ]
    }
  4. 策略配置完成後,單擊繼續編輯基本資料

  5. 基本資料地區,填寫策略名稱稱,然後單擊確定

操作五:在存取控制為RAM角色授予許可權

為RAM角色授予建立的自訂許可權,以便該RAM角色被扮演時能擷取所需的許可權。

  1. 在左側導覽列,選擇身份管理 > 角色

  2. 角色頁面,找到目標RAM角色,然後單擊RAM角色右側的新增授權

  3. 新增授權頁面下,選擇自訂策略,選擇已建立的自訂權限原則。

  4. 單擊確定

二、服務端擷取臨時訪問憑證並計算簽名

說明

建議您先將敏感資訊(如accessKeyIdaccessKeySecretroleArn)配置到環境變數,從而避免在代碼裡顯式地配置,降低泄露風險。

您可以僅在當前會話中使用該環境變數,可以參照以下步驟添加臨時環境變數。

Linux系統

  1. 執行以下命令。

    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"
  2. 執行以下命令,驗證該環境變數是否生效。

    echo $OSS_ACCESS_KEY_ID
    echo $OSS_ACCESS_KEY_SECRET
    echo $OSS_STS_ROLE_ARN

macOS系統

  1. 執行以下命令。

    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"
  2. 執行以下命令,驗證該環境變數是否生效。

    echo $OSS_ACCESS_KEY_ID
    echo $OSS_ACCESS_KEY_SECRET
    echo $OSS_STS_ROLE_ARN

Windows系統

  1. 在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"
  2. 開啟一個新的CMD視窗。

  3. 在新的CMD視窗運行以下命令,檢查環境變數是否生效。

    echo $OSS_ACCESS_KEY_ID
    echo $OSS_ACCESS_KEY_SECRET
    echo $OSS_STS_ROLE_ARN

您可以參考以下代碼,在服務端進行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.aliyun.oss.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、host。
    String bucket = "examplebucket";
    String region = "cn-hangzhou";
    String host = "http://examplebucket.oss-cn-hangzhou.aliyuncs.com";
    // 設定上傳回調URL(即回調伺服器位址),必須為公網地址。用於處理應用伺服器與OSS之間的通訊,OSS會在檔案上傳完成後,把檔案上傳資訊通過此回調URL發送給應用伺服器。
    //限定上傳到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()
                // 必填,請確保代碼運行環境設定了環境變數 OSS_STS_ROLE_ARN
                .setRoleArn(System.getenv("OSS_STS_ROLE_ARN"))
                .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;

        //擷取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("x_oss_credential", x_oss_credential);
        response.put("x_oss_date", x_oss_date);
        response.put("signature", signature);
        response.put("security_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

請參考如下代碼來完成服務端擷取臨時訪問憑證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 = 'yourRoleSessionName'  

# 替換為實際的bucket名稱、region_id、host
bucket = 'examplebucket'
region_id = 'cn-hangzhou'
host = 'http://examplebucket.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="127.0.0.1", port=8000)  # 如果需要監聽其他地址如0.0.0.0,需要您自行在服務端添加認證機制

Node.js

執行以下命令安裝依賴。

npm install ali-oss
npm install @alicloud/credentials
npm install express

請參考如下代碼來完成服務端擷取臨時訪問憑證STStoken並構建上傳策略以計算POST簽名。

const express = require('express');
const OSS = require('ali-oss');
const { STS } = require('ali-oss');
const { getCredential } = require('ali-oss/lib/common/signUtils');
const { getStandardRegion } = require('ali-oss/lib/common/utils/getStandardRegion');
const { policy2Str } = require('ali-oss/lib/common/utils/policy2Str');

const app = express();
const PORT = process.env.PORT || 8000; // 服務要求連接埠號碼

// 設定靜態檔案目錄
app.use(express.static('templates'));

const GenerateSignature = async () => {
    // 初始化STS用戶端
    let sts = new STS({
        accessKeyId: process.env.OSS_ACCESS_KEY_ID,  // 從環境變數中擷取RAM使用者的AccessKey ID
        accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET // 從環境變數中擷取RAM使用者的AccessKey Secret
    });

    // 調用assumeRole介面擷取STS臨時訪問憑證
    const result = await sts.assumeRole(process.env.OSS_STS_ROLE_ARN, '', '3600', 'yourRoleSessionName'); // 從環境變數中擷取RAM角色ARN,並設定臨時訪問憑證有效期間為3600秒,角色會話名稱為yourRoleSessionName可自訂

    // 提取臨時訪問憑證中的AccessKeyId、AccessKeySecret和SecurityToken
    const accessKeyId = result.credentials.AccessKeyId;
    const accessKeySecret = result.credentials.AccessKeySecret;
    const securityToken = result.credentials.SecurityToken;

    // 初始化OSS Client
    const client = new OSS({
        bucket: 'examplebucket', // 請替換為目標Bucket名稱
        region: 'cn-hangzhou', // 請替換為標Bucket所在地區
        accessKeyId,
        accessKeySecret,
        stsToken: securityToken,
        refreshSTSTokenInterval: 0,
        refreshSTSToken: async () => {
            const { accessKeyId, accessKeySecret, securityToken } = await client.getCredential();
            return { accessKeyId, accessKeySecret, stsToken: securityToken };
        },
    });

    // 建立表單資料Map
    const formData = new Map();

    // 設定簽名到期時間為目前時間往後推10分鐘 
    const date = new Date();
    const expirationDate = new Date(date);
    expirationDate.setMinutes(date.getMinutes() + 10);

    // 格式化日期為符合ISO 8601標準的UTC時間字串格式
    function padTo2Digits(num) {
        return num.toString().padStart(2, '0');
    }
    function formatDateToUTC(date) {
        return (
            date.getUTCFullYear() +
            padTo2Digits(date.getUTCMonth() + 1) +
            padTo2Digits(date.getUTCDate()) +
            'T' +
            padTo2Digits(date.getUTCHours()) +
            padTo2Digits(date.getUTCMinutes()) +
            padTo2Digits(date.getUTCSeconds()) +
            'Z'
        );
    }
    const formattedDate = formatDateToUTC(expirationDate);

    // 產生x-oss-credential並設定表單資料
    const credential = getCredential(formattedDate.split('T')[0], getStandardRegion(client.options.region), client.options.accessKeyId);
    formData.set('x_oss_date', formattedDate);
    formData.set('x_oss_credential', credential);
    formData.set('x_oss_signature_version', 'OSS4-HMAC-SHA256');

    // 建立policy
    // 樣本policy表單域只列舉必要欄位
    const policy = {
        expiration: expirationDate.toISOString(),
        conditions: [
            { 'bucket': 'examplebucket'}, 
            { 'x-oss-credential': credential },
            { 'x-oss-signature-version': 'OSS4-HMAC-SHA256' },
            { 'x-oss-date': formattedDate },
        ],
    };

    // 如果存在STS Token,添加到策略和表單資料中
    if (client.options.stsToken) {
        policy.conditions.push({ 'x-oss-security-token': client.options.stsToken });
        formData.set('security_token', client.options.stsToken);
    }

    // 產生簽名並設定表單資料
    const signature = client.signPostObjectPolicyV4(policy, date);
    formData.set('policy', Buffer.from(policy2Str(policy), 'utf8').toString('base64'));
    formData.set('signature', signature);

    // 返回表單資料
    return {
        host: `http://${client.options.bucket}.oss-${client.options.region}.aliyuncs.com`, 
        policy: Buffer.from(policy2Str(policy), 'utf8').toString('base64'),
        x_oss_signature_version: 'OSS4-HMAC-SHA256',
        x_oss_credential: credential,
        x_oss_date: formattedDate,
        signature: signature,
        dir: 'user-dir', // 指定上傳到OSS的檔案首碼
        security_token: client.options.stsToken
    };
};

app.get('/get_post_signature_for_oss_upload', async (req, res) => {
    try {
        const result = await GenerateSignature();
        res.json(result); // 返回產生的簽名資料
    } catch (error) {
        console.error('Error generating signature:', error);
        res.status(500).send('Error generating signature');
    }
});

app.listen(PORT, () => {
    console.log(`Server is running on http://127.0.0.1:${PORT}`); // 如果需要監聽其他地址如0.0.0.0,需要您自行在服務端添加認證機制
});

Go

執行以下命令安裝依賴。

go get -u github.com/aliyun/credentials-go
go mod tidy

請參考如下代碼來完成服務端擷取臨時訪問憑證STStoken並構建上傳策略以計算POST簽名。

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "hash"
    "log"
    "net/http"
    "os"
    "time"
    "github.com/aliyun/credentials-go/credentials"
)

// 定義全域變數
var (
    region     string
    bucketName string
    product    = "oss"
)

// PolicyToken 結構體用於儲存產生的表單資料
type PolicyToken struct {
    Policy           string `json:"policy"`
    SecurityToken    string `json:"security_token"`
    SignatureVersion string `json:"x_oss_signature_version"`
    Credential       string `json:"x_oss_credential"`
    Date             string `json:"x_oss_date"`
    Signature        string `json:"signature"`
    Host             string `json:"host"` 
    Dir              string `json:"dir"` 
}

func main() {
    // 定義預設的IP和連接埠字串
    strIPPort := ":8000"
    if len(os.Args) == 3 {
    strIPPort = fmt.Sprintf("%s:%s", os.Args[1], os.Args[2])
    } else if len(os.Args) != 1 {
    fmt.Println("Usage   : go run test1.go                ")
    fmt.Println("Usage   : go run test1.go ip port        ")
    fmt.Println("")
    os.Exit(0)
    }
    // 列印伺服器啟動並執行地址和連接埠
    fmt.Printf("server is running on %s \n", strIPPort)
    // 註冊處理根路徑請求的函數
    http.HandleFunc("/", handlerRequest)
    // 註冊處理擷取簽章要求的函數
    http.HandleFunc("/get_post_signature_for_oss_upload", handleGetPostSignature)
    // 啟動HTTP伺服器
    err := http.ListenAndServe(strIPPort, nil)
    if err != nil {
    strError := fmt.Sprintf("http.ListenAndServe failed : %s \n", err.Error())
    panic(strError)
    }
}

// handlerRequest 函數處理根路徑請求
func handlerRequest(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
    http.ServeFile(w, r, "templates/index.html")
    return
    }
    http.NotFound(w, r)
}

// handleGetPostSignature 函數處理擷取簽章要求
func handleGetPostSignature(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
    response := getPolicyToken()
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Origin", "*") // 允許跨域
    w.Write([]byte(response))
    return
    }
    http.NotFound(w, r)
}

// getPolicyToken 函數產生 OSS 上傳所需的簽名和憑證
func getPolicyToken() string {
    // 設定bucket所處地區
    region = "cn-hangzhou"
    // 替換為您的bucket名稱
    bucketName = "examplebucket"
    // 設定 OSS 上傳地址
    host := fmt.Sprintf("https://%s.oss-%s.aliyuncs.com", bucketName, region)
    // 設定上傳目錄
    dir := "user-dir"

    config := new(credentials.Config).
    SetType("ram_role_arn").
    SetAccessKeyId(os.Getenv("OSS_ACCESS_KEY_ID")).
    SetAccessKeySecret(os.Getenv("OSS_ACCESS_KEY_SECRET")).
    SetRoleArn("OSS_STS_ROLE_ARN").
    SetRoleSessionName("Role_Session_Name").
    SetPolicy("").
    SetRoleSessionExpiration(3600)

    // 根據配置建立憑證提供器
    provider, err := credentials.NewCredential(config)
    if err != nil {
    log.Fatalf("NewCredential fail, err:%v", err)
    }

    // 從憑證提供器擷取憑證
    cred, err := provider.GetCredential()
    if err != nil {
    log.Fatalf("GetCredential fail, err:%v", err)
    }

    // 構建policy
    utcTime := time.Now().UTC()
    date := utcTime.Format("20060102")
    expiration := utcTime.Add(1 * time.Hour)
    policyMap := map[string]any{
    "expiration": expiration.Format("2006-01-02T15:04:05.000Z"),
    "conditions": []any{
    map[string]string{"bucket": bucketName},
    map[string]string{"x-oss-signature-version": "OSS4-HMAC-SHA256"},
    map[string]string{"x-oss-credential": fmt.Sprintf("%v/%v/%v/%v/aliyun_v4_request", *cred.AccessKeyId, date, region, product)},
    map[string]string{"x-oss-date": utcTime.Format("20060102T150405Z")},
    map[string]string{"x-oss-security-token": *cred.SecurityToken},
    },
    }

    // 將policy轉換為 JSON 格式
    policy, err := json.Marshal(policyMap)
    if err != nil {
    log.Fatalf("json.Marshal fail, err:%v", err)
    }


    // 構造待簽名字串(StringToSign)
    stringToSign := base64.StdEncoding.EncodeToString([]byte(policy))

    hmacHash := func() hash.Hash { return sha256.New() }
        // 構建signing key
    signingKey := "aliyun_v4" + *cred.AccessKeySecret
    h1 := hmac.New(hmacHash, []byte(signingKey))
    io.WriteString(h1, date)
    h1Key := h1.Sum(nil)

    h2 := hmac.New(hmacHash, h1Key)
    io.WriteString(h2, region)
    h2Key := h2.Sum(nil)

    h3 := hmac.New(hmacHash, h2Key)
    io.WriteString(h3, product)
    h3Key := h3.Sum(nil)

    h4 := hmac.New(hmacHash, h3Key)
    io.WriteString(h4, "aliyun_v4_request")
    h4Key := h4.Sum(nil)

    // 產生簽名
    h := hmac.New(hmacHash, h4Key)
    io.WriteString(h, stringToSign)
    signature := hex.EncodeToString(h.Sum(nil))

    // 構建返回給前端的表單
    policyToken := PolicyToken{
    Policy:           stringToSign,
    SecurityToken:    *cred.SecurityToken,
    SignatureVersion: "OSS4-HMAC-SHA256",
    Credential:       fmt.Sprintf("%v/%v/%v/%v/aliyun_v4_request", *cred.AccessKeyId, date, region, product),
    Date:             utcTime.UTC().Format("20060102T150405Z"),
    Signature:        signature,
    Host:             host, // 返回 OSS 上傳地址
    Dir:              dir,  // 返回上傳目錄
    }

    response, err := json.Marshal(policyToken)
    if err != nil {
    fmt.Println("json err:", err)
    }
    return string(response)
}

PHP

執行以下命令安裝依賴。

composer install

請參考如下代碼來完成服務端擷取臨時訪問憑證STStoken並構建上傳策略以計算POST簽名。

<?php

// 引入阿里雲SDK
require_once __DIR__ . '/vendor/autoload.php';

use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use AlibabaCloud\Sts\Sts;

// 禁用錯誤顯示
ini_set('display_errors', '0');


$bucket = 'examplebucket'; // 替換為您的Bucket名稱
$region_id = 'cn-hangzhou'; // 替換為您的Bucket所在地區
$host = 'http://examplebucket.oss-cn-hangzhou.aliyuncs.com'; // 替換為您的Bucket網域名稱
$expire_time = 3600; // 到期時間,單位為秒
$upload_dir = 'user-dir'; // 上傳檔案的首碼

// 計算HMAC-SHA256
function hmacsha256($key, $data) {
    return hash_hmac('sha256', $data, $key, true);
}

// 處理擷取POST簽名的請求
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_SERVER['REQUEST_URI'] === '/get_post_signature_for_oss_upload') {

    AlibabaCloud::accessKeyClient(getenv('OSS_ACCESS_KEY_ID'), getenv('OSS_ACCESS_KEY_SECRET'))
        ->regionId('cn-hangzhou')
        ->asDefaultClient();
    // 建立STS請求。
    $request = Sts::v20150401()->assumeRole();
    // 發起STS請求並擷取結果。
    // 將<YOUR_ROLE_SESSION_NAME>設定為自訂的會話名稱,例如oss-role-session。
    // 將<YOUR_ROLE_ARN>替換為擁有上傳檔案到指定OSS Bucket許可權的RAM角色的ARN。
    $result = $request
        ->withRoleSessionName('oss-role-session')
        ->withDurationSeconds(3600)
        ->withRoleArn(getenv('OSS_STS_ROLE_ARN'))
        ->request();
    // 擷取STS請求結果中的憑證資訊。
    $tokenData = $result->get('Credentials');
    // 構建返回的JSON資料。
    $tempAccessKeyId = $tokenData['AccessKeyId'];
    $tempAccessKeySecret = $tokenData['AccessKeySecret'];
    $securityToken = $tokenData['SecurityToken'];

    $now = time();
    $dtObj = gmdate('Ymd\THis\Z', $now);
    $dtObj1 = gmdate('Ymd', $now);
    $dtObjPlus3h = gmdate('Y-m-d\TH:i:s.u\Z', strtotime('+3 hours', $now));

    // 構建Policy
    $policy = [
        "expiration" => $dtObjPlus3h,
        "conditions" => [
            ["x-oss-signature-version" => "OSS4-HMAC-SHA256"],
            ["x-oss-credential" => "{$tempAccessKeyId}/{$dtObj1}/cn-hangzhou/oss/aliyun_v4_request"],
            ["x-oss-security-token" => $securityToken],
            ["x-oss-date" => $dtObj],
        ]
    ];

    $policyStr = json_encode($policy);

    // 構造待簽名字串
    $stringToSign = base64_encode($policyStr);

    // 計算SigningKey
    $dateKey = hmacsha256(('aliyun_v4' . $tempAccessKeySecret), $dtObj1);
    $dateRegionKey = hmacsha256($dateKey, 'cn-hangzhou');
    $dateRegionServiceKey = hmacsha256($dateRegionKey, 'oss');
    $signingKey = hmacsha256($dateRegionServiceKey, 'aliyun_v4_request');

    // 計算Signature
    $result = hmacsha256($signingKey, $stringToSign);
    $signature = bin2hex($result);

    // 返回簽名資料
    $responseData = [
        'policy' => $stringToSign,
        'x_oss_signature_version' => "OSS4-HMAC-SHA256",
        'x_oss_credential' => "{$tempAccessKeyId}/{$dtObj1}/cn-hangzhou/oss/aliyun_v4_request",
        'x_oss_date' => $dtObj,
        'signature' => $signature,
        'host' => $host,
        'dir' => $upload_dir,
        'security_token' => $securityToken
    ];

    header('Content-Type: application/json');
    echo json_encode($responseData);
    exit;
}

// 首頁路由
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_SERVER['REQUEST_URI'] === '/') {
    echo file_get_contents(__DIR__ . '/public/index.html');
    exit;
}

// 其他路由
http_response_code(404);
echo json_encode(['message' => 'Not Found']);
exit;
?>

步驟三:配置Web端

Web端構造並提交表單上傳請求

當Web端從服務端接收到所有必需的資訊後,就可以構建HTML表單請求了。此請求將直接與OSS服務進行通訊,從而實現檔案的上傳。

Web端接收到的響應樣本

商務服務器向Web端返回STS Token和上傳策略。

<!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>
{
    "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日期和時間標準,例如20231203T121212Z

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端簽名直傳功能。

  1. 通過瀏覽器訪問服務端地址,然後點擊上傳按鈕選擇上傳的檔案。效果樣本如下:

  2. Bucket列表頁面,選擇您之前建立的用來存放使用者上傳檔案的Bucket並開啟,您可以在上傳列表中看到您通過Web端上傳的檔案。

    image

建議配置

限制檔案上傳的許可權和約束條件

您可以根據實際需求修改代碼中的policy ,以設定通過HTML表單上傳檔案到OSS時的許可權限制和約束條件。policy表單域採用JSON格式定義,通過設定不同的參數來限制上傳操作,確保上傳過程的安全性和合規性,例如允許上傳的Bucket名稱、Object首碼、有效期間、允許的HTTP方法、上傳內容的大小限制、內容類型限制等。

{
  "expiration": "2023-12-03T13:00:00.000Z",
  "conditions": [
    {"bucket": "examplebucket"},
    {"x-oss-signature-version": "OSS4-HMAC-SHA256"},
    {"x-oss-credential": "AKIDEXAMPLE/20231203/cn-hangzhou/oss/aliyun_v4_request"},
    {"x-oss-security-token": "CAIS******"},
    {"x-oss-date": "20231203T121212Z"},
    ["content-length-range", 1, 10],
    ["eq", "$success_action_status", "201"],
    ["starts-with", "$key", "user/eric/"],
    ["in", "$content-type", ["image/jpg", "image/png"]],
    ["not-in", "$cache-control", ["no-cache"]]
  ]
}

policy詳細說明如下:

欄位

類型

是否必選

描述

Conditions匹配方式

content-length-range

字串

上傳Object的最小和最大允許大小,單位為位元組。

content-length-range

success_action_status

字串

上傳成功後的返回狀態代碼。

eq、starts-with、in和not-in

key

字串

上傳的Object名稱。

eq、starts-with、in和not-in

content-type

字串

限制上傳的檔案類型。

eq、starts-with、in和not-in

cache-control

字串

指定Object的緩衝行為。

eq、starts-with、in和not-in

關於Policy參數的更多說明請參見Post Policy

服務端簽名直傳並設定上傳回調

如果您需要擷取更多關於使用者上傳檔案的資訊,例如檔案名稱、圖片大小等,請使用上傳回調方案。通過設定上傳回調,您可以在使用者上傳檔案後,自動接收到相關檔案資訊。關於如何佈建服務端簽名直傳並設定上傳回調,請參見伺服器端簽名直傳並設定上傳回調

配置CORS規則時將來源設為伺服器位址

在之前的操作步驟中,為了簡化流程,將允許的跨域請求來源設定為萬用字元 *。然而,出於安全考慮,建議您對來源進行更嚴格的限制。為此,您可以將建立的OSS Bucket的跨域資源共用來源參數設定為您商務服務器的具體地址。這樣一來,唯有來自您指定伺服器的請求才能被授權執行跨網域作業,有效增強了系統的安全性。

參數

樣本值

來源

http://商務服務器地址

允許Methods

POST、PUT、GET

允許Headers

*

清理資源

在本方案中,您建立了1台ECS執行個體、1個OSS Bucket、1個RAM使用者和1個RAM角色。測試完方案後,您可以參考以下規則處理對應產品的資源,避免繼續產生費用或產生安全風險。

釋放ECS執行個體

如果您不再需要這台執行個體,可以將其釋放。釋放後,執行個體停止計費,資料不可恢複。具體操作如下:

  1. 返回Elastic Compute Service控制台執行個體列表頁面,根據地區、執行個體ID找到目標ECS執行個體,單擊操作列下的image

  2. 選擇釋放image

  3. 確認執行個體無誤後,選擇立即釋放,單擊下一步

  4. 確認即將釋放的關聯資源,並瞭解相關資料風險後,單擊確認,即可完成ECS執行個體的釋放。

說明
  • 系統硬碟、分配的公網IP將隨執行個體釋放。

  • 安全性群組、交換器和VPC不會隨執行個體釋放,但它們均為免費資源,您可根據實際的業務需要選擇性刪除。

  • Elastic IP Address不會隨執行個體釋放,且不是免費資源,您可根據實際的業務需要選擇性刪除。

刪除Bucket

  1. 登入OSS管理主控台

  2. 單擊Bucket 列表,然後單擊目標Bucket名稱。

  3. 刪除Bucket的所有檔案(Object)。

  4. 在左側導覽列,單擊移除Bucket,然後按照頁面指引完成刪除操作。

刪除RAM使用者

  1. 使用Resource Access Management員登入RAM控制台

  2. 在左側導覽列,選擇身份管理 > 使用者

  3. 使用者頁面,單擊目標RAM使用者操作列的刪除

    您也可以選中多個RAM使用者,然後單擊使用者列表下方的刪除使用者,批量將多個RAM使用者移入資源回收筒。

  4. 刪除使用者對話方塊,仔細閱讀刪除影響,然後輸入目標RAM使用者名稱稱,最後單擊移入資源回收筒

刪除RAM角色

  1. 使用Resource Access Management員登入RAM控制台

  2. 在左側導覽列,選擇身份管理 > 角色

  3. 角色頁面,單擊目標RAM角色操作列的刪除角色

  4. 刪除角色對話方塊,輸入RAM角色名稱,然後單擊刪除角色

    說明

    如果RAM角色被授予了權限原則,刪除角色時,會同時解除授權。

常見問題

是否可以支援分區上傳大檔案、斷點續傳?

此方案是使用HTML表單上傳的方式上傳檔案,不支援基於分區上傳大檔案和基於分區斷點續傳的情境。如果您想實現分區上傳大檔案或斷點續傳,請參考服務端產生STS臨時訪問憑證

如何防止上傳的檔案被覆蓋

如果希望防止檔案覆蓋,可以在上傳請求的Header中添加x-oss-forbid-overwrite參數,並將其值設為true。例如:

formData.append('x-oss-forbid-overwrite', 'true');