All Products
Search
Document Center

Intelligent Media Management:Build a web application to directly upload data to OSS

Last Updated:Dec 11, 2024

Accessing Intelligent Media Management (IMM) from a browser environment is less secure than from a server environment, especially when the access is based on long-term or fixed access credentials. To reduce the risk of AccessKey pair leakage, you can use Security Token Service (STS) of Alibaba Cloud to issue STS tokens that have a validity period and the required permissions based on the principle of least privilege. This prevents long-term static AccessKey pairs from being exposed due to the less secure browser environment.

Background information

One of the core benefits of STS is the ability to authorize others to access resources without exposing the AccessKey pair of the owning Alibaba Cloud account. AccessKey pair exposure at the level of Alibaba Cloud accounts can cause serious security consequences. For example, data within a compromised Alibaba Cloud account can be manipulated or stolen by attackers. Assume that the technical team of an enterprise wants to allow their web application users to directly upload files from browsers to the Object Storage Service (OSS) bucket managed by the enterprise and use IMM to process the data in the bucket for better concurrency, performance, and stability.

Security risks

Direct interaction with IMM from browsers eliminates the need to use the application server as an intermediary for all business requests and brings lower server-side workload and higher performance.

The enterprise plans to build a data processing service that implements direct upload from the web application by using the following solution:

image

Browser environments are on the user side and cannot be controlled by the enterprise. As a result, the following challenges arise:

  • Credential leaks: Browsers are considered untrusted environments. Saving AccessKey pairs in browsers poses a high risk to credential security.

  • Coarse access control: In most cases, Resource Access Management (RAM) users of the application are granted excessive access permissions to manage and access other cloud services. A leak of the AccessKey pair of such a RAM user causes more serious consequences.

Solution

To address the risks, the technical team adds temporary access authorization to the originally proposed solution. This way, the enterprise can maintain upload efficiency and receive the following benefits:

  • Enhanced authentication and authorization: STS generates tokens that are valid within a specific period of time. This significantly reduces security risks even if the tokens are leaked because the tokens quickly become invalid.

  • Fine-grained access control: STS allows the technical team to configure permissions in conformance to the principle of least privilege. The technical team can grant only the required permissions to the web application. Fine-grained access control mitigates the dangers from credential leaks.

Eventually, the technical team uses the following solution to build the data processing service:

image

Deployment

This section shows how to deploy a browser-based data processing service for the web application by using OSS, IMM, and STS.

Preparations

  • Create an OSS bucket and an IMM project.

    Parameter

    Example

    Region

    China (Hangzhou)

    Bucket Name

    web-direct-upload

    Project Name

    web-direct-project

    For more information, see Create a bucket and Create a project.

  • Create an Elastic Compute Service (ECS) instance, which is used as the application server to obtain STS tokens.

    Note

    If you integrate the STS API operation that is used to obtain STS tokens into your own application server, you can skip this step.

    Parameter

    Example

    Billing Method

    Pay-as-you-go

    Region

    China (Hangzhou)

    Public IP Address

    Assign Public IPv4 Address

    Security Group

    HTTP-TCP:80-open

    For more information, see Create and manage an ECS instance in the console (express version).

  • Configure cross-origin resource sharing (CORS) for the OSS bucket.

    Parameter

    Example

    Source

    http://ECS-public-IP-address

    Allowed Methods

    PUT

    Allowed Headers

    *

    For more information, see CORS.

Procedure

Step 1: Create a RAM user in the RAM console

Create a RAM user and obtain the AccessKey pair of the RAM user. When you create the RAM user, set the Access Mode parameter to OpenAPI Access. The AccessKey pair is used as a long-term access credential for accessing and managing your application server.

  1. Log on to the RAM console by using an Alibaba Cloud account or as an account administrator.

  2. In the left-side navigation pane, choose Identities > Users.

  3. On the Users page, click Create User.

  4. Configure the Logon Name and Display Name parameters.

  5. In the Access Mode section, select OpenAPI Access and click OK.

  6. Click Copy in the Actions section to copy the AccessKey pair (includes an AccessKey ID and an AccessKey secret) and paste the pair into a securely stored file.

Step 2: Grant the RAM user the permissions to call the AssumeRole operation in the RAM console

After the RAM user is created, grant the RAM user the permissions to call the AssumeRole operation of STS. This way, the RAM user can assume a RAM role to obtain STS tokens.

  1. In the left-side navigation pane, choose Identities > Users.

  2. On the Users page, find the RAM user and click Add Permissions in the Actions column.

  3. In the Grant Permission panel, select the AliyunSTSAssumeRoleAccess system policy.

    Note

    The AliyunSTSAssumeRoleAccess policy allows a RAM user to call the AssumeRole operation. The permissions of the policy are independent of the permissions required for the RAM user to obtain STS tokens and initiate requests to IMM.

  4. Click Grant permissions.

Step 3: Create a RAM role in the RAM console

Create a RAM role for the Alibaba Cloud account and obtain the Alibaba Cloud Resource Name (ARN) of the RAM role. The RAM role is assumed by the RAM user later.

  1. In the left-side navigation pane, choose Identities > Roles.

  2. On the Roles page, click Create Role. In the Select Role Type step of the Create Role wizard, set the Select Trusted Entity parameter to Alibaba Cloud Account and click Next.

  3. In the Configure Role step of the Create Role wizard, enter a name in the RAM Role Name field and set the Select Trusted Alibaba Cloud Account parameter to Current Alibaba Cloud Account.

  4. Click OK. After the RAM role is created, click Close.

  5. On the Roles page, enter the RAM role name, such as imm-web-upload, in the search box. Then, click the name of the role.

  6. In the Basic Information section, click Copy next to the ARN field to record the ARN of the RAM role.

    image

Step 4: Create custom policies in the RAM console

Create a custom policy for OSS

Create a custom policy that allows the RAM role to upload data to the specified bucket.

  1. In the left-side navigation pane, choose Permissions > Policies.

  2. On the Policies page, click Create Policy.

  3. On the Create Policy page, click JSON. Copy the following sample script and paste it into the editor. Replace <BucketName> with the name of the bucket that you created earlier. In this example, the bucket name is web-direct-upload.

Important

The following sample script is provided only for reference. You must configure fine-grained RAM policies based on your requirements to avoid granting excessive permissions to users. For more information about how to configure fine-grained policies, see Custom policies for OSS.

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "oss:PutObject",
      "Resource": "acs:oss:*:*:<BucketName>/uploads/*"
    }
  ]
}
  1. Click Next to edit policy information.

  2. In the Basic Information section, enter a policy name and click OK.

Create a custom policy for IMM

Repeat the steps described in Create a custom policy for OSS to create a custom policy that allows the RAM role to perform operations only on the specified project.

The following script provides a sample policy.

Important

The following sample script is provided only for reference. You must configure fine-grained RAM policies based on your requirements to avoid granting excessive permissions to users. For more information about how to configure fine-grained policies, see Grant permissions to a RAM user.

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

Step 5: Attach the custom policies to the RAM role in the RAM console

Attach the custom policies to the RAM role. This way, the RAM role can provide the required permissions when the RAM role is assumed.

  1. In the left-side navigation pane, choose Identities > Roles.

  2. On the Roles page, find the RAM role and click Grant Permission in the Actions column.

  3. In the Policy section of the Grant Permission panel, select Custom Policy from the policy type drop-down list and select the preceding custom policies.

  4. Click Grant permissions.

Step 6: Obtain STS tokens from your application server

Open your web application and integrate STS SDK into your application server to implement the /get_sts_token operation. For more information, see STS SDK overview. When you call the /get_sts_token operation by using the HTTP GET method, the system generates and returns an STS token.

The following procedure describes how to build a web application by using the Flask framework on your ECS instance and obtain STS tokens from the application server:

  1. Connect to the ECS instance.

    For more information, see Create and manage an ECS instance in the console (express version).

  2. Install Python 3.

  3. Create a project folder and switch to the project directory.

    mkdir my_web_sample
    cd my_web_sample
  4. Install dependencies.

    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. Write backend code.

    1. Create a file named main.py.

    2. Add the following Python code to the file:

      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__)
      
      # Replace <YOUR_ROLE_ARN> with the ARN of the RAM role. 
      role_arn_for_oss_upload = '<YOUR_ROLE_ARN>'
      # Specify the region of STS. Example: 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():
          # If you do not specify parameters when you initialize CredentialClient, the default credential chain is used. 
          # If you run the file on your computer, you can specify the AccessKey pair by configuring the ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables.
          # If you run the file on ECS, Elastic Container Instance (ECI), or Container Service for Kubernetes (ACK), you can specify the role of the bound instance by configuring the ALIBABA_CLOUD_ECS_METADATA environment variable. The SDK automatically obtains STS tokens. 
          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,
              # Replace <YOUR_ROLE_SESSION_NAME> with the custom name of the 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
      
      app.run(host="0.0.0.0", port=80) 
    3. Replace <YOUR_ROLE_ARN> in the sample code with the ARN of the RAM role that you created in Step 3.

    4. Replace <YOUR_ROLE_SESSION_NAME> with the custom name of the session. Example: role_session_test.

  6. Start the application by using the AccessKey pair that you obtained in Step 1.

    ALIBABA_CLOUD_ACCESS_KEY_ID=<YOUR_AK_ID> ALIBABA_CLOUD_ACCESS_KEY_SECRET=<YOUR_AK_SECRET> python3 main.py
  7. Visit http://<Public IP address of the ECS instance>/get_sts_token in your browser.

    The following figure shows a success response.

    sts token.png

Step 7: Use an STS token in your browser to initiate a request to IMM

After you configure the /get_sts_token operation on the application server, import OSS SDK for JavaScript on the frontend page of the web application by using Alibaba Cloud CDN to listen for data uploads. When you upload data to the bucket, call the /get_sts_token operation to obtain an STS token and use the STS token to upload the data. When you send a request from a browser to IMM, you must encrypt data by using the Hash-based Message Authentication Code (HMAC). In this example, the data encryption feature is implemented by using the HmacSHA1 function from the crypto.js library. This example integrates the DetectImageFaces operation.

The following steps show how to integrate the IMM operation into the frontend of the web application:

  1. Press Ctrl+C to stop the web application.

  2. Create a frontend project directory.

    mkdir templates
  3. Create an HTML file.

    1. Create a file named imm_example.html in the templates directory.

      vim templates/imm_example.html
    2. Add the following HTML code to the HTML file: The following sample code implements the file upload and image face detection features.

      <!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. Upload a file to OSS:</h2>
            <form id="oss-upload-form">
              <div>
                <label for="file" class="form-label">Select File:</label>
                <input
                  type="file"
                  class="form-control"
                  id="file"
                  name="file"
                  required
                />
              </div>
              <button type="submit" class="btn btn-primary">Upload</button>
            </form>
            <div id="oss-result"></div>
          </div>
          <div class="app">
            <h2>2. Call the DetectImageFaces operation of IMM:</h2>
            <form id="imm-form">
              <button type="submit">Request</button>
            </form>
          </div>
          <div>
            <h2>3. Show the result:</h2>
            <div id="result"></div>
          </div>
          <script>
            /**
             * Check whether the STS token expires. 
             **/
            function isCredentialsExpired(credentials) {
              if (!credentials) {
                return true;
              }
              const expireDate = new Date(credentials.Expiration);
              const now = new Date();
              // If the remaining validity period of the STS token is less than 1 minute, the STS token is considered expired. 
              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();
              // Re-obtain an STS token only when the original one expires. This reduces the number of calls to STS.
              if (isCredentialsExpired(credentials)) {
                const response = await fetch("/get_sts_token", {
                  method: "GET",
                });
                if (!response.ok) {
                  // Handle the errors based on the HTTP status codes.
                  throw new Error(
                    `Failed to obtain an STS token: ${response.status} ${response.statusText}`
                  );
                }
                credentials = await response.json();
              }
              const client = new OSS({
                // Replace <YOUR_BUCKET> with the name of the OSS bucket. 
                bucket: "web-direct-upload",
                // Replace <YOUR_REGION> with the ID of the region in which the OSS bucket is located. Example: 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 = "The upload is successful.";
              }
            });
      
            // ##################################################
      
            // ################# 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);
              // Use the HmacSHA1 method to calculate the HMAC.
              const hmac = CryptoJS.HmacSHA1(data, keyData);
              // Convert the HMAC (CryptoJS object) to a Base64-encoded string.
              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) {
                // Handle the errors based on the HTTP status codes.
                throw new Error(
                  `Failed to obtain an STS token: ${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(
                  `The operation failed: ${response.status} ${response.statusText}`
                );
              }
              return await response.json();
            }
      
            const immForm = document.querySelector("#imm-form");
            immForm.addEventListener("submit", (e) => {
              e.preventDefault();
      
              detect({
                // Specify the IMM operation.
                Action: "DetectImageFaces",
                // Replace <YOUR_PROJECT_NAME> with the IMM project name.
                ProjectName: "web-direct-project",
                //Replace <YOUR_OSS_FILE_PATH> with the path to the object in the bucket: Example: oss://imm-example-cn-hangzhou/test.jpg.
                SourceURI: `oss://web-direct-upload/${uploadResult.name}`,
                // Replace <YOUR_END_POINT> with the endpoint of IMM. Example: 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. Start the application by using the AccessKey pair that you obtained in Step 1.

    ALIBABA_CLOUD_ACCESS_KEY_ID=<YOUR_AK_ID> ALIBABA_CLOUD_ACCESS_KEY_SECRET=<YOUR_AK_SECRET> python3 main.py
  5. Visit http://<Public IP address of the ECS instance>/imm in your browser. On the web page, upload the image from which you want to detect face information.

    image

Solution verification and resource cleanup

Solution verification

After the upload operation is complete, check whether the object exists in the bucket.

  1. Log on to the OSS console.

  2. In the left-side navigation pane, click Buckets.

  3. On the Buckets page, find the OSS bucket that you want to manage and click the bucket name.

  4. On the Objects page, check whether the object exists.

    image

After the data processing request is complete, check whether the face detection result appears on the web page.

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"
        }
    ]
}

Resource cleanup

In this solution implementation, you created an ECS instance and an OSS bucket. After you test the solution, consider the following resource cleanup operations to avoid unnecessary costs: