如何在微信小程序环境下将文件上传到OSS

微信小程序可以将图片、文档、视频等文件上传到OSS,实现文件的云端存储和分发。

方案概览

微信小程序上传文件到OSS的过程如下:

image

要实现微信小程序上传文件到OSS,只需两步:

  1. 配置服务端:在ECS,创建一个实例,用于从STS服务获取一个临时访问凭证,然后使用临时访问凭证为微信小程序生成上传文件到OSS所需的凭证签名。

  2. 配置微信小程序:在小程序平台,使用Bucket域名配置微信小程序的合法域名,确保小程序向OSS发送文件的请求不会被微信拦截;在微信小程序端,实现从ECS获取签名并使用签名上传文件到OSS的功能。

操作步骤

步骤一:配置服务端

说明

在实际部署时,如果您已经有自己的ECS服务器,则无需创建该ECS实例,可直接进行第2ECS服务端计算签名

  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

  2. ECS服务端计算签名。

    重要

    服务端提供了以下两种方式获取STS临时访问凭证并计算签名。

    • ECS扮演RAM角色获取STS临时访问凭证计算签名:服务端不保留AK信息,采用ECS扮演RAM角色的方式,去访问STS服务以获取临时访问凭证并计算签名,最大程度上降低了AK信息泄露的风险,安全性较高。

    • RAM用户获取STS临时访问凭证计算签名:服务端需要保留AK信息,通过获取配置于服务端环境变量里的RAM用户AK信息,进而访问STS服务获取临时访问凭证并计算签名,安全性较低。

    ECS扮演RAM角色获取STS临时访问凭证计算签名

    1. ECS绑定RAM角色。

      操作步骤

      1. 进入RAM访问控制的创建角色页面。

      2. 在创建角色页面勾选阿里云服务,单击下一步

        image

      3. 勾选普通服务角色,填写角色名称并选择受信服务为云服务器后,单击完成,即可完成创建。

        image

      4. 创建完毕后单击为角色授权 > 新增授权,在权限策略中勾选ALiyunOSSFullAccess权限后单击确认新增授权

        说明

        倘若您对访问控制权限较为熟悉,那么可以直接进入创建权限策略页面,按需创建更为精准的自定义授权,随后将其授予RAM角色,以此防止权限出现冗余情况。

        image

      5. 进入云服务器ECS实例页面,在页面上方选择ECS实例所处地域,然后在实例页面单击目标实例右侧image按钮,单击授予/收回RAM角色

        image

      6. 授予/收回RAM角色弹出框选择目标RAM角色,完成ECS绑定RAM角色。

        image

    2. 服务端计算签名。

      Java

      请参考以下示例完成Java服务端的V4签名计算。完整示例工程请部署upload_server.zip

      • 配置依赖。

        <!-- 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>
      • API接口示例。

        package com.example.demo.controller;
        
        import com.example.demo.util.ECSGenerateSignature;
        import com.example.demo.util.RAMGenerateSignature;
        import com.fasterxml.jackson.core.JsonProcessingException;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.RestController;
        
        @RestController
        public class VxController {
        
            /**
             * 使用ECS扮演RAM角色方式获取临时访问凭证,计算签名信息,返回小程序端。
             * @return
             * @throws JsonProcessingException
             */
            @GetMapping("/generate_signature")
            public String generate_signature() throws JsonProcessingException {
                ECSGenerateSignature ecsGenerateSignature = new ECSGenerateSignature();
                return ecsGenerateSignature.getSignature();
            }
        
        }
        
      • 签名信息工具类示例。

        package com.example.demo.util;
        
        import com.aliyun.credentials.models.CredentialModel;
        import com.aliyun.oss.common.auth.Credentials;
        import com.aliyun.oss.common.auth.CredentialsProvider;
        import com.aliyun.oss.common.auth.DefaultCredentials;
        import com.aliyun.oss.common.utils.BinaryUtil;
        import com.fasterxml.jackson.core.JsonProcessingException;
        import com.fasterxml.jackson.databind.ObjectMapper;
        import org.apache.commons.codec.binary.Base64;
        
        import javax.crypto.Mac;
        import javax.crypto.spec.SecretKeySpec;
        import java.time.Instant;
        import java.time.ZoneId;
        import java.time.ZonedDateTime;
        import java.time.format.DateTimeFormatter;
        import java.util.ArrayList;
        import java.util.HashMap;
        import java.util.List;
        import java.util.Map;
        
        /**
         * ECS扮演RAM角色方式获取STS临时访问凭证计算签名
         */
        public class ECSGenerateSignature {
        
            public String getSignature() throws JsonProcessingException {
        
                com.aliyun.credentials.models.Config config = new com.aliyun.credentials.models.Config();
                config.setType("ecs_ram_role");   //固定值无需更改
                config.setRoleName("roleName");   //请替换为ECS扮演的角色名称
                final com.aliyun.credentials.Client credentialsClient = new com.aliyun.credentials.Client(config);
                //创建一个匿名内部类实现CredentialsProvider接口,用于提供阿里云OSS操作所需的凭证信息
                CredentialsProvider credentialsProvider = new CredentialsProvider() {
                    @Override
                    public void setCredentials(Credentials credentials) {
                    }
        
                    @Override
                    public Credentials getCredentials() {
                        CredentialModel credential = credentialsClient.getCredential();
                        return new DefaultCredentials(credential.getAccessKeyId(), credential.getAccessKeySecret(), credential.getSecurityToken());
                    }
        
                };
        
                String accessKeyId = credentialsProvider.getCredentials().getAccessKeyId();   //获取AK
                String secretAccessKey = credentialsProvider.getCredentials().getSecretAccessKey();   //获取SK
                String securityToken = credentialsProvider.getCredentials().getSecurityToken();   //获取Token
        
                //格式化请求日期
                long now = System.currentTimeMillis() / 1000;
                ZonedDateTime dtObj = ZonedDateTime.ofInstant(Instant.ofEpochSecond(now), ZoneId.of("UTC"));
                ZonedDateTime dtObjPlus3h = dtObj.plusHours(3);
                //请求时间
                DateTimeFormatter dtObj1Formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
                String dtObj1 = dtObj.format(dtObj1Formatter);
                //请求日期
                DateTimeFormatter dtObj2Formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
                String dtObj2 = dtObj.format(dtObj2Formatter);
                //请求过期时间
                DateTimeFormatter expirationTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
                String expirationTime = dtObjPlus3h.format(expirationTimeFormatter);
        
                // 创建policy
                ObjectMapper mapper = new ObjectMapper();
        
                Map<String, Object> policy = new HashMap<>();
                policy.put("expiration", expirationTime);
        
                List<Object> conditions = new ArrayList<>();
        
                Map<String, String> bucketCondition = new HashMap<>();
                bucketCondition.put("bucket", "bucketname");    //请替换为目标Bucket名称
                conditions.add(bucketCondition);
        
                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",  accessKeyId + "/" + dtObj2 + "/cn-hangzhou/oss/aliyun_v4_request"); // 替换为实际的 access key id
                conditions.add(credentialCondition);
        
                Map<String, String> token = new HashMap<>();
                token.put("x-oss-security-token", securityToken);
                conditions.add(token);
        
                Map<String, String> dateCondition = new HashMap<>();
                dateCondition.put("x-oss-date", dtObj1);
                conditions.add(dateCondition);
        
                policy.put("conditions", conditions);
                String jsonPolicy = mapper.writeValueAsString(policy);
        
                //构造待签名字符串(StringToSign)
                String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
        
                //计算SigningKey
                byte[] dateKey = hmacsha256(("aliyun_v4" + secretAccessKey).getBytes(), dtObj2);
                byte[] dateRegionKey = hmacsha256(dateKey, "cn-hangzhou");
                byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
                byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
        
                //计算Signature
                byte[] result = hmacsha256(signingKey, stringToSign);
                String signature = BinaryUtil.toHex(result);
        
                Map<String, String> messageMap = new HashMap<>();
                messageMap.put("security_token", securityToken);
                messageMap.put("signature", signature);
                messageMap.put("x_oss_date", dtObj1);
                messageMap.put("x_oss_credential", accessKeyId + "/" + dtObj2 + "/cn-hangzhou/oss/aliyun_v4_request");
                messageMap.put("x_oss_signature_version", "OSS4-HMAC-SHA256");
                messageMap.put("policy", stringToSign);
                ObjectMapper objectMapper = new ObjectMapper();
                //打印返回至客户端的签名信息
                System.out.println(objectMapper.writeValueAsString(messageMap));
                return objectMapper.writeValueAsString(messageMap);
            }
        
        
            /**
             * 使用HMAC-SHA256算法计算给定密钥和数据的哈希值的静态方法
             * @param key
             * @param data
             * @return
             */
            public static byte[] hmacsha256(byte[] key,String data){
                try {
                    SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
                    Mac mac = Mac.getInstance("HmacSHA256");
                    mac.init(secretKeySpec);
                    byte[] hmacBytes = mac.doFinal(data.getBytes());
                    return hmacBytes;
                }catch (Exception e){
                    throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
                }
            }
        }
        

      Python

      请参考以下示例完成Python服务端的V4签名计算。

      • 配置依赖。

        sudo pip install oss2  
        sudo pip install alibabacloud_credentials
      • 代码示例。

        from flask import Flask, jsonify
        import base64
        import hmac
        import hashlib
        import os
        import datetime
        import json
        import time
        from alibabacloud_credentials.client import Client as CredClient
        from alibabacloud_credentials.models import Config as CredConfig
        
        app = Flask(__name__)
        
        
        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('/generate_signature', methods=['GET'])
        def generate_signature():
            """
            处理生成签名信息的请求,执行相关逻辑流程,包括获取环境变量、创建策略、构造待签名字符串、计算签名等操作,
            并返回生成的签名信息。
            :return: JSON格式的响应,包含签名信息的字典,格式如下:
            {
                "policy": "policy字符串",
                "x-oss-signature-version": "OSS4-HMAC-SHA256",
                "x-oss-credential": "accesskeyid/日期/cn-hangzhou/oss/aliyun_v4_request",
                "x-oss-date": "请求时间",
                "signature": "签名",
                "security_token": "安全令牌"
            }
            """
            credentialConfig = CredConfig(
                # 固定值无需更改
                type='ecs_ram_role',
                # 请替换为ECS扮演的角色名称
                role_name='role_name'
            )
            credentialsClient = CredClient(credentialConfig)
            credential = credentialsClient.get_credential()
        
            # 获取AK
            accesskeyid = credential.access_key_id
            # 获取SK
            accesskeysecret = credential.access_key_secret
            # 获取Token
            security_token = credential.security_token
        
            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')
        
            # 步骤1:创建policy。
            policy = {
                "expiration": expiration_time,
                "conditions": [
                    {"bucket": "bucket_name"},   #请替换为目标Bucket名称
                    {"x-oss-signature-version": "OSS4-HMAC-SHA256"},
                    {"x-oss-credential": f"{accesskeyid}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request"},
                    {"x-oss-security-token": security_token},
                    {"x-oss-date": dt_obj_1},
                ]
            }
            policy_str = json.dumps(policy).strip()
        
            # 步骤2:构造待签名字符串(StringToSign)
            stringToSign = base64.b64encode(policy_str.encode()).decode()
        
            # 步骤3:计算SigningKey
            dateKey = hmacsha256(("aliyun_v4" + accesskeysecret).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()
        
            return jsonify({
                "policy": stringToSign,  #表单域
                "x_oss_signature_version": "OSS4-HMAC-SHA256",  #指定签名的版本和算法,固定值为OSS4-HMAC-SHA256
                "x_oss_credential": f"{accesskeyid}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request",  #指明派生密钥的参数集
                "x_oss_date": dt_obj_1,  #请求的时间
                "signature": signature,  #签名认证描述信息
                "security_token": security_token  #安全令牌
            })
        
        
        if __name__ == "__main__":
            app.run(host='0.0.0.0', port=5000)

    RAM用户获取STS临时访问凭证计算签名

    说明
    • 请确保您的RAM用户具备上传文件至OSS以及访问STS服务的权限,如AliyunOSSFullAccessAliyunSTSAssumeRoleAccess权限。具体操作:进入RAM用户页面,选中目标用户后单击右侧添加权限,接着在权限策略处添加相应权限。

    • 若您对访问控制权限较为熟悉,那么可以直接进入创建权限策略页面,按需创建更为精准的自定义授权,随后将其授予RAM用户,以此防止权限出现冗余情况。

    Java

    请参考以下示例完成Java服务端的V4签名计算。完整示例工程请部署upload_server.zip

    • 配置依赖。

      <dependency>
          <groupId>com.aliyun.oss</groupId>
          <artifactId>aliyun-sdk-oss</artifactId>
          <version>3.17.4</version>
      </dependency>
    • 配置环境变量。

      说明
      • <ALIBABA_CLOUD_ACCESS_KEY_ID><ALIBABA_CLOUD_ACCESS_KEY_SECRET>请分别替换为RAM用户的AccessKey ID、AccessKeySecret。如何创建AccessKey IDAccessKeySecret请参见创建AccessKey

      • <ROLE_ARN>请替换为目标角色ARN,在角色页面单击目标RAM角色名称,然后在基本信息区域查看对应的ARN。如何创建角色请参见创建RAM角色并授权

      • macOS/Linux/Unix系统。

        export OSS_ACCESS_KEY_ID=<ALIBABA_CLOUD_ACCESS_KEY_ID>
        export OSS_ACCESS_KEY_SECRET=<ALIBABA_CLOUD_ACCESS_KEY_SECRET>
        export OSS_STS_ROLE_ARN=<ROLE_ARN>
      • Windows系统。

        set OSS_ACCESS_KEY_ID=<ALIBABA_CLOUD_ACCESS_KEY_ID>
        set OSS_ACCESS_KEY_SECRET=<ALIBABA_CLOUD_ACCESS_KEY_SECRET>
        set OSS_STS_ROLE_ARN=<ROLE_ARN>
    • API接口示例。

      package com.example.demo.controller;
      
      import com.example.demo.util.ECSGenerateSignature;
      import com.example.demo.util.RAMGenerateSignature;
      import com.fasterxml.jackson.core.JsonProcessingException;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class VxController {
      
          /**
           * 使用环境变量中的AK获取临时访问凭证,计算签名信息,返回小程序端。
           * @return
           * @throws JsonProcessingException
           */
          @GetMapping("/generate_signature")
          public String generate_signature() throws JsonProcessingException {
              RAMGenerateSignature ramGenerateSignature = new RAMGenerateSignature();
              return ramGenerateSignature.getSignature();
          }
      
      }
      
    • 签名信息工具类示例。

      package com.example.demo.util;
      
      import com.aliyun.oss.common.utils.BinaryUtil;
      import com.fasterxml.jackson.core.JsonProcessingException;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.apache.commons.codec.binary.Base64;
      import com.aliyuncs.DefaultAcsClient;
      import com.aliyuncs.IAcsClient;
      import com.aliyuncs.auth.sts.AssumeRoleRequest;
      import com.aliyuncs.auth.sts.AssumeRoleResponse;
      import com.aliyuncs.exceptions.ClientException;
      import com.aliyuncs.profile.DefaultProfile;
      
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.time.Instant;
      import java.time.ZoneId;
      import java.time.ZonedDateTime;
      import java.time.format.DateTimeFormatter;
      import java.util.ArrayList;
      import java.util.HashMap;
      import java.util.List;
      import java.util.Map;
      
      
      /**
       * 使用环境变量中的AK获取临时访问凭证计算签名
       */
      public class RAMGenerateSignature {
      
          public String getSignature() throws JsonProcessingException {
      
              //获取发送STS请求基础信息
              String accessKeyId = System.getenv("OSS_ACCESS_KEY_ID");    //环境变量中获取access_key_id
              String accessKeySecret = System.getenv("OSS_ACCESS_KEY_SECRET");    //环境变量中获取access_key_secret
              String roleArnForOssUpload = System.getenv("OSS_STS_ROLE_ARN");    //环境变量中获取ARN
              String regionId = "cn-hangzhou";    //发起STS请求所在的地域
              String roleSessionName = "<YOUR_ROLE_SESSION_NAME>";    //色会话名称,用来区分不同的令牌,可自定义
              Long durationSeconds = 3600L;   //临时访问凭证的有效时间
      
              //初始化客户端
              DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
              IAcsClient client = new DefaultAcsClient(profile);
      
              AssumeRoleRequest request = new AssumeRoleRequest();
              request.setRoleArn(roleArnForOssUpload);
              request.setRoleSessionName(roleSessionName);
              request.setDurationSeconds(durationSeconds);
      
              //定义STS临时访问凭证变量
              String STSaccessKeyId = null;
              String STSsecretAccessKey = null;
              String securityToken = null;
      
              try {
                  AssumeRoleResponse response = client.getAcsResponse(request);
      
                  //将请求返回的STS临时访问凭证赋值到自定义变量中
                  STSaccessKeyId = response.getCredentials().getAccessKeyId();
                  STSsecretAccessKey = response.getCredentials().getAccessKeySecret();
                  securityToken = response.getCredentials().getSecurityToken();
      
              } catch (ClientException e) {
                  e.printStackTrace();
              }
      
              //格式化请求日期
              long now = System.currentTimeMillis() / 1000;
              ZonedDateTime dtObj = ZonedDateTime.ofInstant(Instant.ofEpochSecond(now), ZoneId.of("UTC"));
              ZonedDateTime dtObjPlus3h = dtObj.plusHours(3);
              //请求时间
              DateTimeFormatter dtObj1Formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
              String dtObj1 = dtObj.format(dtObj1Formatter);
              //请求日期
              DateTimeFormatter dtObj2Formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
              String dtObj2 = dtObj.format(dtObj2Formatter);
              //请求过期时间
              DateTimeFormatter expirationTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
              String expirationTime = dtObjPlus3h.format(expirationTimeFormatter);
      
              // 创建policy
              ObjectMapper mapper = new ObjectMapper();
      
              Map<String, Object> policy = new HashMap<>();
              policy.put("expiration", expirationTime);
      
              List<Object> conditions = new ArrayList<>();
      
              Map<String, String> bucketCondition = new HashMap<>();
              bucketCondition.put("bucket", "bucketname");  //请替换为目标bucket名称
              conditions.add(bucketCondition);
      
              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",  STSaccessKeyId + "/" + dtObj2 + "/cn-hangzhou/oss/aliyun_v4_request"); // 替换为实际的 access key id
              conditions.add(credentialCondition);
      
              Map<String, String> token = new HashMap<>();
              token.put("x-oss-security-token", securityToken);
              conditions.add(token);
      
              Map<String, String> dateCondition = new HashMap<>();
              dateCondition.put("x-oss-date", dtObj1);
              conditions.add(dateCondition);
      
              policy.put("conditions", conditions);
              String jsonPolicy = mapper.writeValueAsString(policy);
      
              //构造待签名字符串(StringToSign)
              String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
      
              //计算SigningKey
              byte[] dateKey = hmacsha256(("aliyun_v4" + STSsecretAccessKey).getBytes(), dtObj2);
              byte[] dateRegionKey = hmacsha256(dateKey, "cn-hangzhou");
              byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
              byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
      
              //计算Signature
              byte[] result = hmacsha256(signingKey, stringToSign);
              String signature = BinaryUtil.toHex(result);
      
              Map<String, String> messageMap = new HashMap<>();
              messageMap.put("security_token", securityToken);
              messageMap.put("signature", signature);
              messageMap.put("x_oss_date", dtObj1);
              messageMap.put("x_oss_credential", STSaccessKeyId + "/" + dtObj2 + "/cn-hangzhou/oss/aliyun_v4_request");
              messageMap.put("x_oss_signature_version", "OSS4-HMAC-SHA256");
              messageMap.put("policy", stringToSign);
              ObjectMapper objectMapper = new ObjectMapper();
              //打印返回至客户端的签名信息
      //        System.out.println(objectMapper.writeValueAsString(messageMap));
              return objectMapper.writeValueAsString(messageMap);
          }
      
          /**
           * 使用HMAC-SHA256算法计算给定密钥和数据的哈希值的静态方法
           * @param key
           * @param data
           * @return
           */
          public static byte[] hmacsha256(byte[] key,String data){
              try {
                  SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
                  Mac mac = Mac.getInstance("HmacSHA256");
                  mac.init(secretKeySpec);
                  byte[] hmacBytes = mac.doFinal(data.getBytes());
                  return hmacBytes;
              }catch (Exception e){
                  throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
              }
          }
      }
      

    Python

    请参考以下示例完成Python服务端的V4签名计算。

    • 配置依赖。

      pip install oss2  
      pip install alibabacloud_tea_openapi alibabacloud_sts20150401 alibabacloud_credentials
    • 配置环境变量。

      说明
      • <ALIBABA_CLOUD_ACCESS_KEY_ID><ALIBABA_CLOUD_ACCESS_KEY_SECRET>请分别替换为RAM用户的AccessKey ID、AccessKeySecret。如何创建AccessKey IDAccessKeySecret请参见创建AccessKey

      • <ROLE_ARN>请替换为目标角色ARN,在角色页面单击目标RAM角色名称,然后在基本信息区域查看对应的ARN。如何创建角色请参见创建RAM角色并授权

      • macOS/Linux/Unix

        export OSS_ACCESS_KEY_ID=<ALIBABA_CLOUD_ACCESS_KEY_ID>
        export OSS_ACCESS_KEY_SECRET=<ALIBABA_CLOUD_ACCESS_KEY_SECRET>
        export OSS_STS_ROLE_ARN=<ROLE_ARN>
      • Windows

        set OSS_ACCESS_KEY_ID=<ALIBABA_CLOUD_ACCESS_KEY_ID>
        set OSS_ACCESS_KEY_SECRET=<ALIBABA_CLOUD_ACCESS_KEY_SECRET>
        set OSS_STS_ROLE_ARN=<ROLE_ARN>
    • 代码示例。

      from flask import Flask, jsonify
      import base64
      import hmac
      import hashlib
      import os
      import datetime
      import json
      import time
      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
      
      app = Flask(__name__)
      
      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('/generate_signature', methods=['GET'])
      def generate_signature():
          """
          处理生成签名信息的请求,执行相关逻辑流程,包括获取环境变量、创建策略、构造待签名字符串、计算签名等操作,
          并返回生成的签名信息。
          :return: JSON格式的响应,包含签名信息的字典,格式如下:
          {
              "policy": "policy字符串",
              "x-oss-signature-version": "OSS4-HMAC-SHA256",
              "x-oss-credential": "accesskeyid/日期/cn-hangzhou/oss/aliyun_v4_request",
              "x-oss-date": "请求时间",
              "signature": "签名",
              "security_token": "安全令牌"
          }
          """
          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'  
      
          # 指定过期时间,单位为秒
          expire_time = 3600  
      
          bucket = 'examplebucket'
          region_id = 'cn-hangzhou'
      
          # 初始化配置,直接传递凭据
          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 返回的临时凭据
          sts_accesskeyid = token_data['AccessKeyId']
          sts_accesskeysecret = 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')
      
          # 步骤1:创建policy。
          policy = {
              "expiration": expiration_time,
              "conditions": [
                  {"bucket": "cjl3"},   #请替换为目标Bucket名称
                  {"x-oss-signature-version": "OSS4-HMAC-SHA256"},
                  {"x-oss-credential": f"{sts_accesskeyid}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request"},
                  {"x-oss-security-token": security_token},
                  {"x-oss-date": dt_obj_1},
              ]
          }
          policy_str = json.dumps(policy).strip()
      
          # 步骤2:构造待签名字符串(StringToSign)
          stringToSign = base64.b64encode(policy_str.encode()).decode()
      
          # 步骤3:计算SigningKey
          dateKey = hmacsha256(("aliyun_v4" + sts_accesskeysecret).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()
      
          return jsonify({
              "policy": stringToSign,
              "x_oss_signature_version": "OSS4-HMAC-SHA256",
              "x_oss_credential": f"{sts_accesskeyid}/{dt_obj_2}/cn-hangzhou/oss/aliyun_v4_request",
              "x_oss_date": dt_obj_1,
              "signature": signature,
              "security_token": security_token
          })
      
      
      if __name__ == "__main__":
          app.run(host='0.0.0.0', port=5000)

步骤二:配置微信小程序

  1. 为了确保小程序向OSS发送文件的请求不会被微信拦截,在小程序平台,使用Bucket域名配置微信小程序的合法域名。

    1. 进入Bucket列表,选择目标Bucket后在Bucket内的左侧导航栏,单击概览,然后在访问端口区域复制Bucket名。

      image

    2. 登录微信公众平台,将上传和下载的合法域名填写为Bucket的外网访问域名。如图所示。

      说明

      实际业务中,建议您将OSS提供的外网域名和您自己的域名进行绑定,以便使用自定义域名访问OSS存储空间中的文件。配置步骤,请参见绑定自定义域名

      image

  2. 在微信小程序端,使用从ECS服务端获取到的V4签名凭证信息,发送请求上传文件到OSS。

    说明

    关于上传参数详细说明,请参见签名版本4表单元素

    小程序端上传文件index.js文件示例代码如下,完整示例工程请部署uploadoss .zip

    Page({
      data: {
        key: 'filename.txt',  //上传文件名称
        policy: '',
        xOssSecurityToken: '',
        xOssSignatureVersion: '',
        xOssCredential: '',
        xOssDate: '',
        xOssSignature: ''
      },
    
      //上传文件方法 
      uploadFileToOSS(filePath, callback) {
        const {
          key,
          policy,
          xOssSecurityToken,
          xOssSignatureVersion,
          xOssCredential,
          xOssDate,
          xOssSignature
        } = this.data;
       
        const apiUrl='http://<ECS实例公网IP地址>:5000/generate_signature'   //请将ip地址和端口替换为实际服务器公网ip地址及端口
        // 发送请求获取签名信息 
        wx.request({
          url: apiUrl,
          success: (res) => {
          
            this.data.xOssSignatureVersion = res.data.x_oss_signature_version;
            this.data.xOssCredential = res.data.x_oss_credential;
            this.data.xOssDate = res.data.x_oss_date;
            this.data.xOssSignature = res.data.signature;
            this.data.xOssSecurityToken = res.data.security_token;
            this.data.policy = res.data.policy;
            
            //上传参数 
            const formData = {
              key,  //上传文件名称
              policy: this.data.policy,   //表单域
              'x-oss-signature-version': this.data.xOssSignatureVersion,    //指定签名的版本和算法
              'x-oss-credential': this.data.xOssCredential,   //指明派生密钥的参数集
              'x-oss-date': this.data.xOssDate,   //请求的时间
              'x-oss-signature': this.data.xOssSignature,   //签名认证描述信息
              'x-oss-security-token': this.data.xOssSecurityToken,    //安全令牌
              success_action_status: "200"    //上传成功后响应状态码
            };
    
            // 发送请求上传文件 
            wx.uploadFile({
              url: 'https://*********.aliyuncs.com',  // Bucket域名 请替换为目标Bucket域名
              filePath: filePath,
              name: 'file',   //固定值为file
              formData: formData,
              success(res) {
                console.log('上传响应:', res);
                if (res.statusCode === 200) {
                  callback(null, res.data); // 上传成功
                } else {
                  console.error('上传失败,状态码:', res.statusCode);
                  console.error('失败响应:', res);
                  callback(res); // 上传失败,返回响应
                }
              },
              fail(err) {
                console.error('上传失败:', err); // 输出错误信息
                wx.showToast({ title: '上传失败,请重试!', icon: 'none' });
                callback(err); // 调用回调处理错误
              }
            });
          },
          fail: (err) => {
            console.error('请求接口失败:', err);
            wx.showToast({ title: '获取上传参数失败,请重试!', icon: 'none' });
          }
        });
      },
    
      //点击上传文件按钮触发上传文件代码逻辑  
      chooseAndUploadFile() {
        wx.chooseMessageFile({
          count: 1, // 选择一个文件
          type: 'all', // 支持所有类型的文件
          success: (res) => {
            console.log('选择的文件:', res.tempFiles); // 输出选择的文件信息
            if (res.tempFiles.length > 0) {
              const tempFilePath = res.tempFiles[0].path; // 获取选择的文件路径
              console.log('选择的文件路径:', tempFilePath); // 输出文件路径
              this.uploadFileToOSS(tempFilePath, (error, data) => {
                if (error) {
                  wx.showToast({ title: '上传失败!', icon: 'none' });
                  console.error('上传失败:', error); // 输出具体的错误信息
                } else {
                  wx.showToast({ title: '上传成功!', icon: 'success' });
                  console.log('上传成功:', data); // 输出上传成功后的数据
                }
              });
            } else {
              wx.showToast({ title: '未选择文件!', icon: 'none' });
            }
          },
          fail: (err) => {
            wx.showToast({ title: '选择文件失败!', icon: 'none' });
            console.error('选择文件失败:', err); // 输出选择文件的错误信息
          }
        });
      }
    });

结果验证

  1. 编译运行后,在微信小程序界面单击上传文件

    微信小程序上传文件到OSS.gif

  2. Bucket列表页面,选择上传文件的Bucket并打开,单击右上角任务列表 > 上传列表 您可以在上传列表中看到您通过小程序上传的文件。

    image