本文由簡體中文內容自動轉碼而成。阿里雲不保證此自動轉碼的準確性、完整性及時效性。本文内容請以簡體中文版本為準。

搭建Web應用檔案直傳服務

更新時間:2024-12-10 20:08

通過瀏覽器環境訪問IMM與服務端環境訪問相比,存在更高的安全風險,尤其是在處理長期和固定的存取金鑰時。為降低密鑰泄露的風險,您可以使用阿里雲提供的Security Token Service(STS)為瀏覽器端發放具有時間限制和最小必要許可權的臨時身份憑證,從而避免將長期的靜態存取金鑰暴露在較不安全的瀏覽器環境中。

背景資訊

STS解決的一個核心問題是如何在不暴露阿里雲帳號的AccessKey的情況下安全地授權其他人訪問。因為阿里雲帳號的AccessKey泄露會帶來極大的安全風險,其他人可以隨意操作該帳號下所有的資源、盜取重要訊息等。為了更好地服務使用者並提高應用程式的效能和穩定性,企業A的技術團隊決定實現Web應用的終端使用者在瀏覽器直接上傳檔案到OSS並與IMM互動,以滿足並發大規模資料處理的需求。

安全風險

為了減少企業的商務服務器負擔並提高效率,該Web應用被設計為在使用者的網頁瀏覽器中直接與IMM互動,而非所有請求都通過企業的商務服務器中轉。

企業A計劃採取以下方案搭建Web應用檔案直傳服務:

由於網頁瀏覽器運行環境完全處於使用者端且不受企業A直接控制,企業A面臨以下挑戰:

  • 密鑰泄露:考慮到網頁瀏覽器歸使用者掌控且為不可信環境,RAM使用者的存取金鑰等敏感資訊儲存在瀏覽器中,面臨較高的泄露風險。

  • 許可權過大:當建立RAM使用者作為商務服務器的應用程式的身份時,該使用者通常需要較高許可權以訪問其他雲端服務。如果RAM使用者的存取金鑰儲存在瀏覽器中並被泄露,可能導致許可權被濫用,增加安全風險。

解決方案

為了應對上述風險,企業A可以在原有方案的基礎上增加臨時授權。通過這種方式,企業A能夠在確保資料直傳效率的同時,實現以下效果:

  • 增強身分識別驗證和授權:通過STS產生的具有時間限制的令牌,即便在短時間內泄露,也極大降低了安全風險。因為這些憑證很快就會失效,降低了被不當利用的可能性。

  • 精微調權限控制:STS允許根據最小許可權原則配置許可權,僅授權Web應用必需的存取權限。這種精細化的許可權控制方法限制了潛在泄露的影響範圍,防止了過度許可權的風險。

企業A最終採取以下方案搭建IMM資料處理服務:

方案部署

下面將以一個簡單的使用者檔案上傳情境為例,引導您一步步使用OSS、IMM和STS為Web應用部署瀏覽器資料處理服務。

準備工作

  • 建立一個OSS Bucket和一個IMM Project。

    參數

    樣本值

    參數

    樣本值

    所屬地區

    華東1(杭州)

    Bucket名稱

    web-direct-upload

    Project 名稱

    web-direct-project

    具體步驟,請參見建立儲存空間建立專案

  • 建立一台ECS執行個體作為商務服務器,用於產生臨時身份憑證。

    說明

    在實際部署時,您可以將調用STS服務的介面整合到自己的商務服務器的介面中,無需建立該ECS執行個體。

    參數

    樣本值

    參數

    樣本值

    付費類型

    隨用隨付

    地區

    華東1(杭州)

    公網 IP

    分配公網 IPv4 地址

    安全性群組

    開放HTTP (TCP:80)連接埠

    具體步驟,請參見通過控制台使用ECS執行個體(快捷版)

  • 為建立的OSS Bucket配置跨域資源共用。

    參數

    樣本值

    參數

    樣本值

    來源

    http://ECS公網IP地址

    允許Methods

    PUT

    允許Headers

    *

    具體步驟,請參見跨網域設定

部署步驟

步驟一:在存取控制建立RAM使用者

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

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

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

  3. 單擊建立使用者

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

  5. 訪問方式地區下,選擇控制台訪問,然後單擊確定

  6. 單擊複製,儲存調用密鑰(AccessKey IDAccessKey Secret)。

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

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

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

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

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

    說明

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

  4. 單擊確認新增授權

步驟三:在存取控制建立RAM角色

為當前雲帳號建立一個RAM角色,並擷取其ARN,用於RAM使用者之後進行扮演。

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

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

  3. 填寫角色名稱,選擇當前雲帳號

  4. 單擊完成。完成角色建立後,單擊關閉

  5. 在RAM角色管理頁面,搜尋方塊輸入角色名稱,例如imm-web-upload

  6. 點擊角色名稱,單擊複製,儲存角色的ARN。

    image

步驟四:在存取控制建立自訂權限原則

建立OSS授權策略

按照最小授權原則,為RAM角色建立一個自訂權限原則,限制只能向指定的OSS Bucket上傳檔案。

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

  2. 單擊建立權限原則

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

重要

以下樣本僅供參考。您需要根據實際需求配置更細粒度的授權策略,防止出現許可權過大的風險。關於更細粒度的授權策略配置詳情,請參見Object Storage Service自訂權限原則參考

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

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

建立IMM授權策略

按照最小授權原則,為RAM角色建立一個自訂權限原則,建立步驟同OSS授權策略,限制只能對指定的IMM專案進行操作。

如下為策略指令碼:

重要

以下樣本僅供參考。您需要根據實際需求配置更細粒度的授權策略,防止出現許可權過大的風險。關於更細粒度的授權策略配置詳情,請參見IMM授權策略參考

{
    "Version": "1",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "imm:*",
            "Resource": "acs:imm:cn-hangzhou:1413397765616316:project/<PROJECT名稱>"
        }
    ]
}

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

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

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

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

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

  4. 單擊確定

步驟六:在商務服務器擷取臨時身份憑證

在Web應用中,通過在商務服務器整合STS SDK,實現一個擷取臨時STS身份憑證的介面。當這個介面(/get_sts_token)通過HTTP GET方法被訪問時,它會產生一個臨時身份憑證,並將其返回給要求者。

在ECS執行個體上,使用Flask架構快速搭建Web應用,實現一個擷取臨時STS身份憑證的介面的操作樣本如下:

  1. 串連ECS執行個體。

    具體操作,請參見通過控制台使用ECS執行個體(快捷版)

  2. 安裝Python3

  3. 建立專案檔夾,然後切換到專案目錄。

    mkdir my_web_sample
    cd my_web_sample
  4. 安裝依賴。

    pip3 install Flask
    pip3 install attr
    pip3 install yarl
    pip3 install async_timeout
    pip3 install idna_ssl
    pip3 install attrs
    pip3 install aiosignal
    pip3 install charset_normalizer
    pip3 install alibabacloud_tea_openapi
    pip3 install alibabacloud_sts20150401
    pip3 install alibabacloud_credentials
  5. 編寫後端代碼。

    1. 建立一個main.py檔案。

    2. 在這個檔案中,添加以下Python代碼。

      import json
      from flask import Flask, render_template
      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
      
      app = Flask(__name__)
      
      # 將<YOUR_ROLE_ARN>替換為RAM角色的ARN。
      role_arn_for_oss_upload = '<YOUR_ROLE_ARN>'
      # 設定為STS服務的地區,例如cn-hangzhou。
      region_id = 'cn-hangzhou'
      @app.route("/imm")
      def imm():
          return render_template('imm_example.html')
      @app.route('/get_sts_token', methods=['GET'])
      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>設定為自訂的會話名稱。
              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
      
      app.run(host="0.0.0.0", port=80) 
    3. 將代碼中的<YOUR_ROLE_ARN>替換為步驟三擷取的角色ARN。

    4. 將代碼中的<YOUR_ROLE_SESSION_NAME>設定為自訂的會話名稱,例如role_session_test

  6. 使用步驟一擷取的存取金鑰啟動應用程式。

    ALIBABA_CLOUD_ACCESS_KEY_ID=<YOUR_AK_ID> ALIBABA_CLOUD_ACCESS_KEY_SECRET=<YOUR_AK_SECRET> python3 main.py
  7. 在瀏覽器中訪問http://<ECS執行個體公網IP地址>/get_sts_token

    成功返回樣本如下:

    sts token.png

步驟七:在瀏覽器使用臨時身份憑證調用IMM服務

在商務服務器配置了擷取STS臨時身份憑證的介面後,在Web應用的前端網頁使用CDN引入OSS JavaScript SDK,實現對檔案上傳的監聽。當使用者上傳檔案,調用/get_sts_token介面從商務服務器請求臨時訪問憑證,然後使用臨時訪問憑證向OSS上傳檔案。當通過Web端調用IMM服務時,需要使用HMAC演算法對資料加密,此例子中使用第三方庫crypto.js中的HmacSHA1來實現,然後使用臨時訪問憑證調用DetectImageFaces介面擷取傳回值。

在ECS上,將前端代碼整合到Web應用的操作樣本如下:

  1. Ctrl + C停止應用程式。

  2. 建立前端專案檔。

    mkdir templates
  3. 建立HTML模板檔案。

    1. templates目錄中建立一個imm_example.html檔案。

      vim templates/imm_example.html
    2. 在這個檔案中,添加以下HTML代碼。該頁面的功能是在瀏覽器中類比使用者選擇並上傳檔案,通過調用IMM服務檢測圖片中的人臉以及人臉資訊

      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          <title>imm_example</title>
          <script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.17.0.min.js"></script>
          <script src="https://unpkg.com/crypto-js@4.1.1/crypto-js.js"></script>
          <style>
            .app form div {
              display: flex;
              margin: 12px;
            }
            .app label {
              display: flex;
              width: 100px;
            }
            .app form input {
              width: 200px;
            }
          </style>
        </head>
        <body>
          <div class="app">
            <h2>1.OSS檔案上傳:</h2>
            <form id="oss-upload-form">
              <div>
                <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 id="oss-result"></div>
          </div>
          <div class="app">
            <h2>2.調用IMM DetectImageFaces服務:</h2>
            <form id="imm-form">
              <button type="submit">Request</button>
            </form>
          </div>
          <div>
            <h2>3.返回結果:</h2>
            <div id="result"></div>
          </div>
          <script>
            /**
             * 判斷臨時憑證是否到期。
             **/
            function isCredentialsExpired(credentials) {
              if (!credentials) {
                return true;
              }
              const expireDate = new Date(credentials.Expiration);
              const now = new Date();
              // 如果有效期間不足一分鐘,視為到期。
              return expireDate.getTime() - now.getTime() <= 60000;
            }
      
            let credentials = null;
            let uploadResult = null;
      
            // ############# OSS-UPLOAD #################
            const ossForm = document.querySelector("#oss-upload-form");
            ossForm.addEventListener("submit", async (event) => {
              event.preventDefault();
              // 臨時憑證到期時,才重新擷取,減少對 sts 服務的調用
              if (isCredentialsExpired(credentials)) {
                const response = await fetch("/get_sts_token", {
                  method: "GET",
                });
                if (!response.ok) {
                  // 處理錯誤的HTTP狀態代碼
                  throw new Error(
                    `擷取STS令牌失敗: ${response.status} ${response.statusText}`
                  );
                }
                credentials = await response.json();
              }
              const client = new OSS({
                // 將<YOUR_BUCKET>設定為OSS Bucket名稱。
                bucket: "web-direct-upload",
                // 將<YOUR_REGION>設定為OSS Bucket所在地區,例如oss-cn-hangzhou。
                region: "oss-cn-hangzhou",
                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);
              if (result) {
                uploadResult = result;
                document.querySelector("#oss-result").textContent = "上傳成功";
              }
            });
      
            // ##################################################
      
            // ################# IMM ############################
            function getTimestamp() {
              const pad2 = (number) => {
                return String(number).padStart(2, "0");
              };
              let date = new Date();
              let YYYY = date.getUTCFullYear();
              let MM = pad2(date.getUTCMonth() + 1);
              let DD = pad2(date.getUTCDate());
              let HH = pad2(date.getUTCHours());
              let mm = pad2(date.getUTCMinutes());
              let ss = pad2(date.getUTCSeconds());
              return `${YYYY}-${MM}-${DD}T${HH}:${mm}:${ss}Z`;
            }
            function generateSecureNonce(length = 32) {
              const array = new Uint8Array(length);
              window.crypto.getRandomValues(array);
              return Array.from(array, (byte) => byte.toString(36))
                .join("")
                .substring(0, length);
            }
      
            function normalize(params) {
              const list = [];
              const flated = params;
              const keys = Object.keys(flated).sort();
              for (let i = 0; i < keys.length; i++) {
                const key = keys[i];
                const value = flated[key];
                list.push([encode(key), encode(value)]);
              }
              return list;
            }
            function canonicalize(normalized) {
              const fields = [];
              for (let i = 0; i < normalized.length; i++) {
                const [key, value] = normalized[i];
                fields.push(key + "=" + value);
              }
              return fields.join("&");
            }
            function encode(str) {
              const result = encodeURIComponent(str);
              return result
                .replace(/!/g, "%21")
                .replace(/'/g, "%27")
                .replace(/\(/g, "%28")
                .replace(/\)/g, "%29")
                .replace(/\*/g, "%2A");
            }
            async function sha1(data, key = null) {
              const keyData = CryptoJS.enc.Utf8.parse(key);
              // 使用HmacSHA1方法計算HMAC
              const hmac = CryptoJS.HmacSHA1(data, keyData);
              // 將HMAC (CryptoJS對象) 轉換為 base64 字串
              const base64Hmac = hmac.toString(CryptoJS.enc.Base64);
              return base64Hmac;
            }
      
            async function getRPCSignature(signedParams, method, secret) {
              const normalized = normalize(signedParams);
              const canonicalized = canonicalize(normalized);
              const stringToSign = `${method}&${encode("/")}&${encode(
                canonicalized
              )}`;
              const key = secret + "&";
              return await sha1(stringToSign, key);
            }
      
            async function getStsToken() {
              const response = await fetch("/get_sts_token", {
                method: "GET",
              });
              if (!response.ok) {
                // 處理錯誤的HTTP狀態代碼
                throw new Error(
                  `擷取STS令牌失敗: ${response.status} ${response.statusText}`
                );
              }
              return await response.json();
            }
      
            // let credentials = null;
            async function getParams({ Action, ProjectName, SourceURI }) {
              if (isCredentialsExpired(credentials)) {
                credentials = await getStsToken();
              }
              const params = {
                AccessKeyId: credentials.AccessKeyId,
                Format: "JSON",
                Action,
                ProjectName,
                Timestamp: getTimestamp(),
                SecureTransport: true,
                SignatureMethod: "HMAC-SHA1",
                SignatureNonce: generateSecureNonce(),
                SignatureVersion: "1.0",
                SourceURI,
                Version: "2020-09-30",
                SecurityToken: credentials.SecurityToken,
              };
              params.Signature = await getRPCSignature(
                params,
                "POST",
                credentials.AccessKeySecret
              );
              return params;
            }
            async function detect(requestParams) {
              const params = await getParams(requestParams);
              const url = `${requestParams.EndPoint}?${new URLSearchParams(
                params
              ).toString()}`;
              const response = await fetch(url, {
                method: "POST",
                headers: {
                  "Content-Type": "application/json",
                },
              });
              if (!response.ok) {
                throw new Error(
                  `介面請求失敗: ${response.status} ${response.statusText}`
                );
              }
              return await response.json();
            }
      
            const immForm = document.querySelector("#imm-form");
            immForm.addEventListener("submit", (e) => {
              e.preventDefault();
      
              detect({
                // imm服務名稱
                Action: "DetectImageFaces",
                // 將<YOUR_PROJECT_NAME>設定為 IMM專案名稱
                ProjectName: "web-direct-project",
                // 將<YOUR_OSS_FILE_PATH>設定為檔案的OSS儲存地址 , 例如:oss://imm-example-cn-hangzhou/test.jpg
                SourceURI: `oss://web-direct-upload/${uploadResult.name}`,
                // 將<YOUR_END_POINT>設定為 IMM服務的endpoint , 例如: https://imm.cn-beijing.aliyuncs.com
                EndPoint: "https://imm.cn-hangzhou.aliyuncs.com",
              })
                .then((res) => {
                  // console.log(res);
                  document.querySelector("#result").textContent = JSON.stringify(res);
                })
                .catch((err) => {
                  document.querySelector("#result").textContent = err;
                });
            });
          </script>
        </body>
      </html>
      
      
  4. 使用步驟一擷取的存取金鑰啟動應用程式。

    ALIBABA_CLOUD_ACCESS_KEY_ID=<YOUR_AK_ID> ALIBABA_CLOUD_ACCESS_KEY_SECRET=<YOUR_AK_SECRET> python3 main.py
  5. 在瀏覽器中訪問http://<ECS執行個體公網IP地址>/imm,然後在頁面中選擇需要分析的圖片進行上傳,類比真實使用者在瀏覽器的行為。

    image

完成與清理

方案驗證

完成以上操作後,您可以查看檔案是否已上傳到OSS。

  1. 登入Object Storage Service控制台

  2. 在左側導覽列,選擇Bucket列表

  3. Bucket列表頁面,單擊目標Bucket。

  4. 檔案清單頁面,查看成功上傳的檔案。

    image

完成以上操作後,您可以查看IMM返回的分析數值。

image

{
    "RequestId": "63661E75-3A41-5FBF-B023-867DA6A6AA81",
    "Faces": [
        {
            "Beard": "none",
            "MaskConfidence": 0.764,
            "Gender": "female",
            "Boundary": {
                "Left": 182,
                "Top": 175,
                "Height": 381,
                "Width": 304
            },
            "BeardConfidence": 0.987,
            "FigureId": "047d8d12-c3b6-4e22-9b9a-b91facc650fb",
            "Mouth": "open",
            "Emotion": "happiness",
            "Age": 45,
            "MouthConfidence": 0.999,
            "FigureType": "face",
            "GenderConfidence": 1,
            "HeadPose": {
                "Pitch": -16.206,
                "Roll": -5.124,
                "Yaw": 3.421
            },
            "Mask": "none",
            "EmotionConfidence": 0.984,
            "HatConfidence": 1,
            "GlassesConfidence": 0.976,
            "Sharpness": 1,
            "FigureClusterId": "figure-cluster-id-unavailable",
            "FaceQuality": 0.942,
            "Attractive": 0.044,
            "AgeSD": 7,
            "Glasses": "glasses",
            "FigureConfidence": 1,
            "Hat": "none"
        }
    ]
}

清理資源

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

  • 本頁導讀 (1, M)
  • 背景資訊
  • 安全風險
  • 解決方案
  • 方案部署
  • 準備工作
  • 部署步驟
  • 完成與清理
文檔反饋
phone 聯絡我們