全部產品
Search
文件中心

Object Storage Service:在用戶端直接上傳檔案到OSS

更新時間:Feb 28, 2024

用戶端直傳是指用戶端直接上傳檔案到Object Storage Service。相對於服務端代理上傳,用戶端直傳避免了商務服務器中轉檔案,提高了上傳速度,節省了伺服器資源。本文介紹用戶端直傳的方案優勢、安全實現和實踐參考。

為什麼用戶端直傳

在典型的服務端和用戶端架構下,常見的檔案上傳方式是服務端代理上傳:用戶端將檔案上傳到商務服務器,然後商務服務器將檔案上傳到OSS。在這個過程中,一份資料需要在網路上傳輸兩次,會造成網路資源的浪費,增大服務端的資源開銷。為瞭解決這一問題,您可以在用戶端直連OSS來完成檔案上傳,無需經過商務服務器中轉。

如何?用戶端直傳

實現用戶端直傳需要解決以下兩大問題:

跨域訪問

如果您的用戶端是Web端或小程式,您需要解決跨域訪問被限制的問題。瀏覽器以及小程式容器出於安全考慮,通常都會限制跨域訪問,這一限制也會限制您的用戶端代碼直連OSS。您可以通過配置OSS Bucket的跨域訪問規則,來允許您的Web端或小程式的網域名稱直接存取OSS。更多資訊,請參見跨網域設定

安全授權

上傳檔案到OSS需要使用RAM使用者的存取金鑰(AccessKey)來完成簽名認證,但是在用戶端中使用長期有效存取金鑰,可能會導致存取金鑰泄露,進而引起安全問題。為瞭解決這一問題,您可以選擇以下方案實現安全上傳:

  • 服務端產生STS臨時訪問憑證

    對於大部分上傳檔案的情境,建議您在服務端使用STS SDK擷取STS臨時訪問憑證,然後在用戶端使用STS臨時憑證和OSS SDK直接上傳檔案。用戶端能重複使用服務端產生的STS臨時訪問憑證產生簽名,因此適用於基於分區上傳大檔案、基於分區斷點續傳的情境。需要注意的是,頻繁地調用STS服務會引起限流,因此建議您對STS臨時憑證做緩衝處理,並在有效期間前重新整理。更多資訊,請參見什麼是STS

  • 服務端產生PostObject所需的簽名和Post Policy

    對於需要限制上傳檔案屬性的情境,您可以在服務端產生PostObject所需的Post簽名、PostPolicy等資訊,然後用戶端可以憑藉這些資訊,在一定的限制下不依賴OSS SDK直接上傳檔案。您可以藉助服務端產生的PostPolicy限制用戶端上傳的檔案,例如限制檔案大小、檔案類型。此方案適用於通過HTML表單上傳的方式上傳檔案。需要注意的是,此方案不支援基於分區上傳大檔案、基於分區斷點續傳的情境。更多資訊,請參見PostObject

  • 服務端產生PutObject所需的簽名URL

    對於簡單上傳檔案的情境,您可以在服務端使用OSS SDK產生PutObject所需的簽名URL,用戶端可以憑藉簽名URL,不依賴OSS SDK直接上傳檔案。需要注意的是,此方案不適用於基於分區上傳大檔案、基於分區斷點續傳的情境。在服務端對每個分區產生簽名URL,並將簽名URL返回給用戶端,會增加與服務端的互動次數和網路請求的複雜性。另外,用戶端可能會修改分區的內容或順序,導致最終合并的檔案不正確。更多資訊,請參見簽名版本1

服務端產生STS臨時訪問憑證

服務端通過STS臨時訪問憑證授權用戶端上傳檔案到OSS的過程如下。

  1. 用戶端向商務服務器請求臨時訪問憑證。

  2. 商務服務器使用STS SDK調用AssumeRole介面,擷取臨時訪問憑證。

  3. STS產生並返回臨時訪問憑證給商務服務器。

  4. 商務服務器返回臨時訪問憑證給用戶端。

  5. 用戶端使用OSS SDK通過該臨時訪問憑證上傳檔案到OSS。

  6. OSS返回成功響應給用戶端。

樣本工程

sts.zip

範例程式碼

服務端程式碼範例

服務端產生臨時訪問憑證的程式碼範例如下:

import json
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

# 將<YOUR_ROLE_ARN>替換為擁有上傳檔案到指定OSS Bucket許可權的RAM角色的ARN。
role_arn_for_oss_upload = '<YOUR_ROLE_ARN>'
# 將<YOUR_REGION_ID>設定為STS服務的地區,例如cn-hangzhou。
region_id = '<YOUR_REGION_ID>'

def get_sts_token():
    # 初始化 CredentialClient 時不指定參數,代表使用預設憑據鏈。
    # 在本地運行程式時,可以通過環境變數 ALIBABA_CLOUD_ACCESS_KEY_ID、ALIBABA_CLOUD_ACCESS_KEY_SECRET 指定 AK;
    # 在 ECS\ECI\Container Service上運行時,可以通過環境變數 ALIBABA_CLOUD_ECS_METADATA 來指定綁定的執行個體\節點角色,SDK 會自動換取 STS 臨時憑證。
    config = Config(region_id=region_id, credential=CredentialClient())
    sts_client = Sts20150401Client(config=config)
    assume_role_request = sts_20150401_models.AssumeRoleRequest(
        role_arn=role_arn_for_oss_upload,
        # 將<YOUR_ROLE_SESSION_NAME>設定為自訂的會話名稱,例如oss-role-session。
        role_session_name='<YOUR_ROLE_SESSION_NAME>'
    )
    response = sts_client.assume_role(assume_role_request)
    token = json.dumps(response.body.credentials.to_map())
    return token

用戶端程式碼範例

Web端使用臨時訪問憑證上傳檔案到OSS的程式碼範例如下:

let credentials = null;
const form = document.querySelector("form");
form.addEventListener("submit", async (event) => {
  event.preventDefault();
  // 臨時憑證到期時,才重新擷取,減少對sts服務的調用。
  if (isCredentialsExpired(credentials)) {
    const response = await fetch("/get_sts_token_for_oss_upload", {
      method: "GET",
    });
    credentials = await response.json();
  }
  const client = new OSS({
    // 將<YOUR_BUCKET>設定為OSS Bucket名稱。
    bucket: "<YOUR_BUCKET>",
    // 將<YOUR_REGION>設定為OSS Bucket所在地區,例如region: 'oss-cn-hangzhou'。
    region: "oss-<YOUR_REGION>",
    accessKeyId: credentials.AccessKeyId,
    accessKeySecret: credentials.AccessKeySecret,
    stsToken: credentials.SecurityToken,
  });

  const fileInput = document.querySelector("#file");
  const file = fileInput.files[0];
  const result = await client.put(file.name, file);
  console.log(result);
});

/**
 * 判斷臨時憑證是否到期。
 **/
function isCredentialsExpired(credentials) {
  if (!credentials) {
    return true;
  }
  const expireDate = new Date(credentials.Expiration);
  const now = new Date();
  // 如果有效期間不足一分鐘,視為到期。
  return expireDate.getTime() - now.getTime() <= 60000;
}

服務端產生PostObject所需的簽名和Post Policy

服務端通過Post簽名和Post Policy授權用戶端上傳檔案到OSS的過程如下。

  1. 用戶端向商務服務器請求Post簽名和Post Policy等資訊。

  2. 商務服務器產生並返回Post簽名和Post Policy等資訊給用戶端。

  3. 用戶端使用Post簽名和Post Policy等資訊調用PostObject通過HTML表單的方式上傳檔案到OSS。

  4. OSS返回成功響應給用戶端。

樣本工程

postsignature.zip

範例程式碼

服務端範例程式碼

服務端產生Post簽名和Post Policy等資訊的程式碼範例如下:

import os
from hashlib import sha1 as sha
import json
import base64
import hmac
import datetime
import time

# 配置環境變數OSS_ACCESS_KEY_ID。
access_key_id = os.environ.get('OSS_ACCESS_KEY_ID')
# 配置環境變數OSS_ACCESS_KEY_SECRET。
access_key_secret = os.environ.get('OSS_ACCESS_KEY_SECRET')
# 將<YOUR_BUCKET>替換為Bucket名稱。
bucket = '<YOUR_BUCKET>'
# host的格式為bucketname.endpoint。將<YOUR_BUCKET>替換為Bucket名稱。將<YOUR_ENDPOINT>替換為OSS Endpoint,例如oss-cn-hangzhou.aliyuncs.com。
host = 'https://<YOUR_BUCKET>.<YOUR_ENDPOINT>'
# 指定上傳到OSS的檔案首碼。
upload_dir = 'user-dir-prefix/'
# 指定到期時間,單位為秒。
expire_time = 3600


def generate_expiration(seconds):
    """
    通過指定有效時間長度(秒)產生到期時間。
    :param seconds: 有效時間長度(秒)。
    :return: ISO8601 時間字串,如:"2014-12-01T12:00:00.000Z"。
    """
    now = int(time.time())
    expiration_time = now + seconds
    gmt = datetime.datetime.utcfromtimestamp(expiration_time).isoformat()
    gmt += 'Z'
    return gmt


def generate_signature(access_key_secret, expiration, conditions, policy_extra_props=None):
    """
    產生簽名字串Signature。
    :param access_key_secret: 有許可權訪問目標Bucket的AccessKeySecret。
    :param expiration: 簽名到期時間,按照ISO8601標準表示,並需要使用UTC時間,格式為yyyy-MM-ddTHH:mm:ssZ。樣本值:"2014-12-01T12:00:00.000Z"。
    :param conditions: 策略條件,用於限制上傳表單時允許設定的值。
    :param policy_extra_props: 額外的policy參數,後續如果policy新增參數支援,可以在通過dict傳入額外的參數。
    :return: signature,簽名字串。
    """
    policy_dict = {
        'expiration': expiration,
        'conditions': conditions
    }
    if policy_extra_props is not None:
        policy_dict.update(policy_extra_props)
    policy = json.dumps(policy_dict).strip()
    policy_encode = base64.b64encode(policy.encode())
    h = hmac.new(access_key_secret.encode(), policy_encode, sha)
    sign_result = base64.b64encode(h.digest()).strip()
    return sign_result.decode()

def generate_upload_params():
    policy = {
        # 有效期間。
        "expiration": generate_expiration(expire_time),
        # 約束條件。
        "conditions": [
            # 未指定success_action_redirect時,上傳成功後的返回狀態代碼,預設為 204。
            ["eq", "$success_action_status", "200"],
            # 表單域的值必須以指定首碼開始。例如指定key的值以user/user1開始,則可以寫為["starts-with", "$key", "user/user1"]。
            ["starts-with", "$key", upload_dir],
            # 限制上傳Object的最小和最大允許大小,單位為位元組。
            ["content-length-range", 1, 1000000],
            # 限制上傳的檔案為指定的圖片類型
            ["in", "$content-type", ["image/jpg", "image/png"]]
        ]
    }
    signature = generate_signature(access_key_secret, policy.get('expiration'), policy.get('conditions'))
    response = {
        'policy': base64.b64encode(json.dumps(policy).encode('utf-8')).decode(),
        'ossAccessKeyId': access_key_id,
        'signature': signature,
        'host': host,
        'dir': upload_dir
        # 可以在這裡再自行追加其他參數
    }
    return json.dumps(response)

用戶端範例程式碼

Web端使用Post簽名和Post Policy等資訊上傳檔案到OSS的程式碼範例如下:

const form = document.querySelector('form');
const fileInput = document.querySelector('#file');
form.addEventListener('submit', (event) => {
  event.preventDefault();
  let file = fileInput.files[0];
  let filename = fileInput.files[0].name;
  fetch('/get_post_signature_for_oss_upload', { method: 'GET' })
    .then(response => response.json())
    .then(data => {
      const formData = new FormData();
      formData.append('name',filename);
      formData.append('policy', data.policy);
      formData.append('OSSAccessKeyId', data.ossAccessKeyId);
      formData.append('success_action_status', '200');
      formData.append('signature', data.signature);
      formData.append('key', data.dir + filename);
      // file必須為最後一個表單域,除file以外的其他表單域無順序要求。
      formData.append('file', file);
      fetch(data.host, { method: 'POST', body: formData },).then((res) => {
        console.log(res);
        alert('檔案已上傳');
      });
    })
    .catch(error => {
      console.log('Error occurred while getting OSS upload parameters:', error);
    });
});

服務端產生PutObject所需的簽名URL

服務端通過簽名URL授權用戶端上傳檔案到OSS的過程如下。

  1. 用戶端向商務服務器請求籤名URL。

  2. 商務服務器使用OSS SDK產生PUT類型的簽名URL,然後將其返回給用戶端。

  3. 用戶端使用PUT類型的簽名URL調用PutObject上傳檔案到OSS。

  4. OSS向用戶端返回成功響應。

樣本工程

presignedurl.zip

範例程式碼

服務端範例程式碼

服務端產生簽名URL的程式碼範例如下:

import oss2
from oss2.credentials import EnvironmentVariableCredentialsProvider

# 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())
# 將<YOUR_ENDPOINT>替換為Bucket所在地區對應的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
# 將<YOUR_BUCKET>替換為Bucket名稱。
bucket = oss2.Bucket(auth, '<YOUR_ENDPOINT>', '<YOUR_BUCKET>')
# 指定到期時間,單位秒。
expire_time = 3600
# 填寫Object完整路徑,例如exampledir/exampleobject.png。Object完整路徑中不能包含Bucket名稱。
object_name = 'exampledir/exampleobject.png'

def generate_presigned_url():
    # 指定Header。
    headers = dict()
    # 指定Content-Type。
    headers['Content-Type'] = 'image/png'
    # 指定儲存類型。
    # headers["x-oss-storage-class"] = "Standard"
    # 產生簽名URL時,OSS預設會對Object完整路徑中的正斜線(/)進行轉義,從而導致產生的簽名URL無法直接使用。
    # 設定slash_safe為True,OSS不會對Object完整路徑中的正斜線(/)進行轉義,此時產生的簽名URL可以直接使用。
    url = bucket.sign_url('PUT', object_name, expire_time, slash_safe=True, headers=headers)
    return url

用戶端範例程式碼

Web端使用簽名URL上傳檔案到OSS的程式碼範例如下:

const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
  event.preventDefault();
  const fileInput = document.querySelector("#file");
  const file = fileInput.files[0];
  fetch(`/get_presigned_url_for_oss_upload?filename=${file.name}`, { method: "GET" })
    .then((response) => {
        return response.text();
     })
    .then((url) => {
      fetch(url, {
        method: "PUT",
        headers: new Headers({
          'Content-Type': 'image/png',
        }),
        body: file,
       }).then((res) => {
            console.log(res);
            alert('檔案已上傳');
       });
   });
});

用戶端直傳實踐參考

不同類型的用戶端的直傳實踐參考如下: