服务端签名直传

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

服务端生成签名以实现Web端直传,使用户能够通过Web浏览器直接使用PostObject接口上传文件到OSS。此过程通过签名机制确保上传的安全性。同时,根据具体业务需求,您可以在服务端生成上传策略(Policy),以限制上传操作。

方案概览

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

image

要实现服务端签名直传,只需3步:

说明

由于使用了临时访问凭证,整个过程中不会泄露业务服务器的长期密钥,保证了文件上传的安全性。

  1. 配置OSS:配置OSS,在控制台创建一个Bucket,用于存储用户上传的文件。同时,为 Bucket 配置跨域资源共享(CORS) 规则,以允许来自服务端的跨域请求。

  2. 配置服务端:配置服务端,调用STS服务获取一个临时访问凭证,然后使用临时访问凭证和服务端预设的上传策略(如Bucket名称、目录路径、过期时间等)生成签名授权用户在一定时间内进行文件上传。

  3. 配置Web:配置Web端,构造HTML表单请求,通过表单提交使用签名将文件上传到OSS。

示例工程

操作步骤

您可以点击一键部署在您的本地服务器快速部署并体验服务端签名直传。部署完成后,复制输出页签下OssClientAddress的地址,通过您的浏览器体验文件直传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');