服务端签名直传

更新时间:
一键部署
我的部署

您可以使用PostObject接口,将文件直接从 Web 端上传到 OSS,服务器生成的签名为直传操作提供安全保障,同时支持配置上传策略(Policy)以限制上传操作并满足业务需求。

方案概览

服务端生成签名实现Web端直传的过程如下:

image

要实现服务端签名直传,只需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. 创建专有网络VPC & 交换机

    创建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。您也可以在创建实例后,为实例绑定弹性公网IP,具体操作,请参见EIP绑定至ECS实例

    说明
    • 若未绑定公网IP,将无法使用SSHRDP通过公网直接访问实例,也无法通过公网验证实例中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服务条款》《云服务器ECS退订说明》,单击确认下单。在提示成功的对话框中,单击管理控制台,即可在控制台查看到创建好的ECS实例。请您保存以下数据,以便在后续操作中使用。

    • 实例ID:便于在实例列表中查询到该实例。

    • 地域:便于在实例列表中查询到该实例。

    • 公网IP地址:便于在后续使用ECS实例时,做Web服务的部署结果验证。

    imageimage

操作二:连接ECS实例

  1. 云服务器ECS控制台实例列表页面,根据地域、实例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 IDAccessKey Secret)。

操作二:在访问控制为RAM用户授予调用AssumeRole接口的权限

创建RAM用户后,需要授予RAM用户调用STS服务的AssumeRole接口的权限,使其可以通过扮演RAM角色来获取临时身份凭证。

  1. 在左侧导航栏,选择身份管理 > 用户

  2. 用户页面,找到目标RAM用户,然后单击RAM用户右侧的添加权限

  3. 新增授权页面,选择AliyunSTSAssumeRoleAccess系统策略。

    说明

    授予RAM用户调用STS服务AssumeRole接口的固定权限是AliyunSTSAssumeRoleAccess,与后续获取临时访问凭证以及通过临时访问凭证发起OSS请求所需权限无关。

    image.png

  4. 单击确认新增授权

操作三:在访问控制创建RAM角色

为当前云账号创建一个RAM角色,并获取对应的角色的ARN(Aliyun Resource Name,阿里云资源名称),用于RAM用户之后进行扮演。

  1. 在左侧导航栏,选择身份管理 > 角色

  2. 单击创建角色,可信实体类型选择阿里云账号,单击下一步

  3. 填写角色名称,选择当前云账号

  4. 单击完成。完成角色创建后,单击关闭

  5. RAM角色管理页面,搜索框输入角色名称,例如oss-web-upload

  6. 单击复制,保存角色的ARN。

    1.png

操作四:在访问控制创建上传文件的权限策略

按照最小授权原则,为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. 单击确定

二、服务端获取临时访问凭证并计算签名

您可以参考以下代码,在服务端进行POST签名版本4(推荐)的计算工作。关于policy表单域详细配置信息,请参见policy表单域

Java

Maven项目中,导入以下依赖。

<!-- https://mvnrepository.com/artifact/com.aliyun/credentials-java -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>credentials-java</artifactId>
    <version>0.3.4</version>
</dependency>

<dependency>
    <groupId>com.aliyun.kms</groupId>
    <artifactId>kms-transfer-client</artifactId>
    <version>0.1.0</version>
</dependency>

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>
<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>sts20150401</artifactId>
  <version>1.1.6</version>
</dependency>

您可以参考如下代码来完成服务端获取临时访问凭证并计算POST签名:

package com.sanfeng.oss.serversigneddirectupload.demos.web;

import com.aliyun.sts20150401.models.AssumeRoleResponse;
import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import com.aliyun.tea.TeaException;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.binary.Base64;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Controller
public class WebController {

    //OSS基础信息 替换为实际的 bucket 名称和 region-id
    String bucket = "web-direct-upload";
    String region = "cn-hangzhou";

    String host = "http://" + bucket + ".oss-" + region + ".aliyuncs.com";
    //指定上传到OSS的文件前缀。
    String upload_dir = "dir";
    //指定过期时间,单位为秒。
    Long expire_time = 3600L;

    /**
     * 通过指定有效的时长(秒)生成过期时间。
     * @param seconds 有效时长(秒)。
     * @return ISO8601 时间字符串,如:"2014-12-01T12:00:00.000Z"。
     */
    public static String generateExpiration(long seconds) {
        // 获取当前时间戳(以秒为单位)
        long now = Instant.now().getEpochSecond();
        // 计算过期时间的时间戳
        long expirationTime = now + seconds;
        // 将时间戳转换为Instant对象,并格式化为ISO8601格式
        Instant instant = Instant.ofEpochSecond(expirationTime);
        // 定义时区
        ZoneId zone = ZoneId.systemDefault();  // 使用系统默认时区
        // 将 Instant 转换为 ZonedDateTime
        ZonedDateTime zonedDateTime = instant.atZone(zone);
        // 定义日期时间格式,例如2023-12-03T13:00:00.000Z
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        // 格式化日期时间
        String formattedDate = zonedDateTime.format(formatter);
        // 输出结果
        return formattedDate;
    }
    //初始化STS Client
    public static com.aliyun.sts20150401.Client createStsClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        // 建议使用更安全的 STS 方式。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                    // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv("OSS_ACCESS_KEY_ID"))
                // 必填,请确保代码运行环境设置了环境变量 OSS_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv("OSS_ACCESS_KEY_SECRET"));
        // Endpoint 请参考 https://api.aliyun.com/product/Sts
        config.endpoint = "sts.cn-hangzhou.aliyuncs.com";
        return new com.aliyun.sts20150401.Client(config);
    }

    //获取STS临时凭证
    public static AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials getCredential() throws Exception {
        com.aliyun.sts20150401.Client client = WebController.createStsClient();
        com.aliyun.sts20150401.models.AssumeRoleRequest assumeRoleRequest = new com.aliyun.sts20150401.models.AssumeRoleRequest()
                .setRoleArn(System.getenv("OSS_ACCESS_KEY_ID"))
                .setRoleSessionName("yourRoleSessionName");// 自定义会话名称
        com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
        try {
            // 复制代码运行请自行打印 API 的返回值
            AssumeRoleResponse response = client.assumeRoleWithOptions(assumeRoleRequest, runtime);
            // credentials里包含了后续要用到的AccessKeyId、AccessKeySecret和SecurityToken。
            return response.body.credentials;
        } catch (TeaException error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            com.aliyun.teautil.Common.assertAsString(error.message);
        } catch (Exception _error) {
            TeaException error = new TeaException(_error.getMessage(), _error);
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            com.aliyun.teautil.Common.assertAsString(error.message);
        }
        return null;
    }
    @GetMapping("/get_post_signature_for_oss_upload")
    public ResponseEntity<Map<String, String>> getPostSignatureForOssUpload() throws Exception {
        AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials sts_data = getCredential();

        String accesskeyid =  sts_data.accessKeyId;
        String accesskeysecret =  sts_data.accessKeySecret;
        String securitytoken =  sts_data.securityToken;
        System.out.println("sts accesskeyid:"+accesskeyid);
        System.out.println("sts accesskeysecret:"+accesskeysecret);
        System.out.println("sts securitytoken:"+securitytoken);

        //获取x-oss-credential里的date,当前日期,格式为yyyyMMdd
        LocalDate today = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        String date = today.format(formatter);

        //获取x-oss-date
        ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC);
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
        String x_oss_date = now.format(formatter2);

        // 步骤1:创建policy。
        String x_oss_credential = accesskeyid + "/" + date + "/" + region + "/oss/aliyun_v4_request";
        
        ObjectMapper mapper = new ObjectMapper();

        Map<String, Object> policy = new HashMap<>();
        policy.put("expiration", generateExpiration(expire_time));

        List<Object> conditions = new ArrayList<>();

        Map<String, String> bucketCondition = new HashMap<>();
        bucketCondition.put("bucket", bucket);
        conditions.add(bucketCondition);

        Map<String, String> securityTokenCondition = new HashMap<>();
        securityTokenCondition.put("x-oss-security-token", securitytoken);
        conditions.add(securityTokenCondition);

        Map<String, String> signatureVersionCondition = new HashMap<>();
        signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256");
        conditions.add(signatureVersionCondition);

        Map<String, String> credentialCondition = new HashMap<>();
        credentialCondition.put("x-oss-credential", x_oss_credential); // 替换为实际的 access key id
        conditions.add(credentialCondition);

        Map<String, String> dateCondition = new HashMap<>();
        dateCondition.put("x-oss-date", x_oss_date);
        conditions.add(dateCondition);

        conditions.add(Arrays.asList("content-length-range", 1, 10240000));
        conditions.add(Arrays.asList("eq", "$success_action_status", "200"));
        conditions.add(Arrays.asList("starts-with", "$key", upload_dir));

        policy.put("conditions", conditions);

        String jsonPolicy = mapper.writeValueAsString(policy);

        // 步骤2:构造待签名字符串(StringToSign)。
        String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
        // System.out.println("stringToSign: " + stringToSign);

        // 步骤3:计算SigningKey。
        byte[] dateKey = hmacsha256(("aliyun_v4" + accesskeysecret).getBytes(), date);
        byte[] dateRegionKey = hmacsha256(dateKey, region);
        byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
        byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
        // System.out.println("signingKey: " + BinaryUtil.toBase64String(signingKey));

        // 步骤4:计算Signature。
        byte[] result = hmacsha256(signingKey, stringToSign);
        String signature = BinaryUtil.toHex(result);
        // System.out.println("signature:" + signature);

        Map<String, String> response = new HashMap<>();
        // 将数据添加到 map 中
        response.put("version", "OSS4-HMAC-SHA256");
        // 这里是易错点,不能直接传policy,需要做一下Base64编码
        response.put("policy", stringToSign);
        response.put("credential", x_oss_credential);
        response.put("ossdate", x_oss_date);
        response.put("signature", signature);
        response.put("token", securitytoken);
        response.put("dir", upload_dir);
        response.put("host", host);
        // 返回带有状态码 200 (OK) 的 ResponseEntity,返回给Web端,进行PostObject操作
        return ResponseEntity.ok(response);
    }
    public static byte[] hmacsha256(byte[] key, String data) {
        try {
            // 初始化HMAC密钥规格,指定算法为HMAC-SHA256并使用提供的密钥。
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");

            // 获取Mac实例,并通过getInstance方法指定使用HMAC-SHA256算法。
            Mac mac = Mac.getInstance("HmacSHA256");
            // 使用密钥初始化Mac对象。
            mac.init(secretKeySpec);

            // 执行HMAC计算,通过doFinal方法接收需要计算的数据并返回计算结果的数组。
            byte[] hmacBytes = mac.doFinal(data.getBytes());

            return hmacBytes;
        } catch (Exception e) {
            throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
        }
    }
}

Python

  • 执行以下命令安装依赖。

    pip install flask
    pip install alibabacloud_tea_openapi alibabacloud_sts20150401 alibabacloud_credentials
  • 请参考如下代码来完成Python服务端获取临时访问凭证STStoken并构建上传策略以计算POST签名。

    from flask import Flask, render_template, jsonify
    from alibabacloud_tea_openapi.models import Config
    from alibabacloud_sts20150401.client import Client as Sts20150401Client
    from alibabacloud_sts20150401 import models as sts_20150401_models
    from alibabacloud_credentials.client import Client as CredentialClient
    import os
    import json
    import base64
    import hmac
    import datetime
    import time
    import hashlib
    
    app = Flask(__name__)
    
    # 配置环境变量 OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_ID, OSS_STS_ROLE_ARN。
    access_key_id = os.environ.get('OSS_ACCESS_KEY_ID')
    access_key_secret = os.environ.get('OSS_ACCESS_KEY_SECRET')
    role_arn_for_oss_upload = os.environ.get('OSS_STS_ROLE_ARN')
    
    # 自定义会话名称
    role_session_name = 'role_session_name'  
    
    # 替换为实际的bucket名称和region_id
    bucket = 'examplebucket'
    region_id = 'cn-hangzhou'
    
    host = f'http://{bucket}.oss-cn-hangzhou.aliyuncs.com'
    
    # 指定过期时间,单位为秒
    expire_time = 3600  
    # 指定上传到OSS的文件前缀。
    upload_dir = 'dir'
    
    def hmacsha256(key, data):
        """
        计算HMAC-SHA256哈希值的函数
    
        :param key: 用于计算哈希的密钥,字节类型
        :param data: 要进行哈希计算的数据,字符串类型
        :return: 计算得到的HMAC-SHA256哈希值,字节类型
        """
        try:
            mac = hmac.new(key, data.encode(), hashlib.sha256)
            hmacBytes = mac.digest()
            return hmacBytes
        except Exception as e:
            raise RuntimeError(f"Failed to calculate HMAC-SHA256 due to {e}")
    
    @app.route("/")
    def hello_world():
        return render_template('index.html')
    
    @app.route('/get_post_signature_for_oss_upload', methods=['GET'])
    def generate_upload_params():
        # 初始化配置,直接传递凭据
        config = Config(
            region_id=region_id,
            access_key_id=access_key_id,
            access_key_secret=access_key_secret
        )
    
        # 创建 STS 客户端并获取临时凭证
        sts_client = Sts20150401Client(config=config)
        assume_role_request = sts_20150401_models.AssumeRoleRequest(
            role_arn=role_arn_for_oss_upload,
            role_session_name=role_session_name
        )
        response = sts_client.assume_role(assume_role_request)
        token_data = response.body.credentials.to_map()
    
        # 使用 STS 返回的临时凭据
        temp_access_key_id = token_data['AccessKeyId']
        temp_access_key_secret = token_data['AccessKeySecret']
        security_token = token_data['SecurityToken']
    
    
        now = int(time.time())
        # 将时间戳转换为datetime对象
        dt_obj = datetime.datetime.utcfromtimestamp(now)
        # 在当前时间增加3小时,设置为请求的过期时间
        dt_obj_plus_3h = dt_obj + datetime.timedelta(hours=3)
    
        # 请求时间
        dt_obj_1 = dt_obj.strftime('%Y%m%dT%H%M%S') + 'Z'
        # 请求日期
        dt_obj_2 = dt_obj.strftime('%Y%m%d')
        # 请求过期时间
        expiration_time = dt_obj_plus_3h.strftime('%Y-%m-%dT%H:%M:%S.000Z')
        
        # 构建 Policy 并生成签名
        policy = {
            "expiration": expiration_time,
            "conditions": [
                ["eq", "$success_action_status", "200"],
                {"x-oss-signature-version": "OSS4-HMAC-SHA256"},
                {"x-oss-credential": f"{temp_access_key_id}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request"},
                {"x-oss-security-token": security_token},
                {"x-oss-date": dt_obj_1},  
            ]
        }
        print(dt_obj_1)
        policy_str = json.dumps(policy).strip()
    
        # 步骤2:构造待签名字符串(StringToSign)
        stringToSign = base64.b64encode(policy_str.encode()).decode()
    
        # 步骤3:计算SigningKey
        dateKey = hmacsha256(("aliyun_v4" + temp_access_key_secret).encode(), dt_obj_2)
        dateRegionKey = hmacsha256(dateKey, "cn-hangzhou")
        dateRegionServiceKey = hmacsha256(dateRegionKey, "oss")
        signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request")
    
        # 步骤4:计算Signature
        result = hmacsha256(signingKey, stringToSign)
        signature = result.hex()
    
        # 组织返回数据
        response_data = {
            'policy': stringToSign,  #表单域
            'x_oss_signature_version': "OSS4-HMAC-SHA256",  #指定签名的版本和算法,固定值为OSS4-HMAC-SHA256
            'x_oss_credential': f"{temp_access_key_id}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request",  #指明派生密钥的参数集
            'x_oss_date': dt_obj_1,  #请求的时间
            'signature': signature,  #签名认证描述信息
            'host': host,
            'dir': upload_dir,
            'security_token': security_token  #安全令牌
    
        }
        return jsonify(response_data)
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0", port=8000)

步骤三:配置Web

Web端构造并提交表单上传请求

Web端从服务端接收到所有必需的信息后,就可以构建HTML表单请求了。此请求将直接与OSS服务进行通信,从而实现文件的上传。

Web接收到的响应示例

业务服务器向Web端返回STS Token和上传策略。

{
    "dir": "user-dirs",
    "host": "http://examplebucket.oss-cn-hangzhou.aliyuncs.com",
    "policy": "eyJl****",
    "security_token": "CAIS****",
    "signature": "9103****",
    "x_oss_credential": "STS.NSpW****/20241127/cn-hangzhou/oss/aliyun_v4_request",
    "x_oss_date": "20241127T060941Z",
    "x_oss_signature_version": "OSS4-HMAC-SHA256"
}

Body中的各字段说明如下:

字段

描述

dir

限制上传的文件前缀。

host

Bucket域名。

policy

用户表单上传的策略(Policy),详情请参见Post Policy

security_token

安全令牌。

signature

Policy签名后的字符串。详情请参见Post Signature

x_oss_credential

指明派生密钥的参数集。

x_oss_date

请求的时间,其格式遵循ISO 8601日期和时间标准,例如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. 通过浏览器访问服务端地址,然后点击上传按钮选择上传的文件。效果示例如下:

    2024-11-08_14-42-06 (1) copy

  2. Bucket列表页面,选择您之前创建的用来存放用户上传文件的Bucket并打开,您可以在上传列表中看到您通过Web端上传的文件。

    image

建议配置

将敏感信息配置为环境变量

建议您把敏感信息(如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

服务端签名直传并设置上传回调

如果您需要获取更多关于用户上传文件的信息,例如文件名称、图片大小等,请使用上传回调方案。通过设置上传回调,您可以在用户上传文件后,自动接收到相关文件信息。关于如何配置服务端签名直传并设置上传回调,请参见服务器端签名直传并设置上传回调

配置CORS规则时将来源设为服务器地址

在之前的操作步骤中,为了简化流程,将允许的跨域请求来源设置为通配符 *。然而,出于安全考虑,建议您对来源进行更严格的限制。为此,您可以将创建的OSS Bucket的跨域资源共享来源参数设置为您业务服务器的具体地址。这样一来,唯有来自您指定服务器的请求才能被授权执行跨域操作,有效增强了系统的安全性。

参数

示例值

来源

http://业务服务器地址

允许Methods

POST、PUT、GET

允许Headers

*

清理资源

在本方案中,您创建了1ECS实例、1OSS Bucket、1RAM用户和1RAM角色。测试完方案后,您可以参考以下规则处理对应产品的资源,避免继续产生费用或产生安全风险。

释放ECS实例

如果您不再需要这台实例,可以将其释放。释放后,实例停止计费,数据不可恢复。具体操作如下:

  1. 返回云服务器ECS控制台实例列表页面,根据地域、实例ID找到目标ECS实例,单击操作列下的image

  2. 选择释放image

  3. 确认实例无误后,选择立即释放,单击下一步

  4. 确认即将释放的关联资源,并了解相关数据风险后,单击确认,即可完成ECS实例的释放。

说明
  • 系统盘、分配的公网IP将随实例释放。

  • 安全组、交换机和VPC不会随实例释放,但它们均为免费资源,您可根据实际的业务需要选择性删除。

  • 弹性公网IP不会随实例释放,且不是免费资源,您可根据实际的业务需要选择性删除。

删除Bucket

  1. 登录OSS管理控制台

  2. 单击Bucket 列表,然后单击目标Bucket名称。

  3. 删除Bucket的所有文件(Object)。

  4. 在左侧导航栏,单击删除Bucket,然后按照页面指引完成删除操作。

删除RAM用户

  1. 使用RAM管理员登录RAM控制台

  2. 在左侧导航栏,选择身份管理 > 用户

  3. 用户页面,单击目标RAM用户操作列的删除

    您也可以选中多个RAM用户,然后单击用户列表下方的删除用户,批量将多个RAM用户移入回收站。

  4. 删除用户对话框,仔细阅读删除影响,然后输入目标RAM用户名称,最后单击移入回收站

删除RAM角色

  1. 使用RAM管理员登录RAM控制台

  2. 在左侧导航栏,选择身份管理 > 角色

  3. 角色页面,单击目标RAM角色操作列的删除角色

  4. 删除角色对话框,输入RAM角色名称,然后单击删除角色

    说明

    如果RAM角色被授予了权限策略,删除角色时,会同时解除授权。

常见问题

是否可以支持分片上传大文件、断点续传?

此方案是使用HTML表单上传的方式上传文件,不支持基于分片上传大文件和基于分片断点续传的场景。如果您想实现分片上传大文件或断点续传,请参考服务端生成STS临时访问凭证

如何防止上传的文件被覆盖

如果希望防止文件覆盖,可以在上传请求的Header中添加x-oss-forbid-overwrite参数,并将其值设为true。例如:

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