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

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

方案概览

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

image

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

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

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

操作步骤

步骤一:配置ECS

说明

在实际部署时,如果您已经有自己的ECS服务器,则无需创建该ECS实例,可直接进行第2步ECS绑定RAM角色

一. 创建并连接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,将无法使用SSH或RDP通过公网直接访问实例,也无法通过公网验证实例中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

二. ECS绑定RAM角色

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

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

    image

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

    image

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

    说明

    如果您熟悉访问控制权限,那么也可以直接进入创建权限策略。这样能够进行更精准的自定义授权,之后再将自定义权限授予所创建的RAM角色。

    image

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

    image

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

    image

三. ECS服务端配置依赖

请执行以下命令安装获取临时访问凭证所需要的依赖项。

Python

执行以下命令,安装Credentials工具。

sudo pip install oss2  
sudo pip install alibabacloud_credentials

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>

四. ECS服务端计算签名

您可以参考以下代码,在ECS服务端进行V4签名的计算工作。关于policy表单域详细配置信息,请参见policy表单域

Python

请参考如下代码来完成Python服务端的V4签名计算。

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)

Java

请参考如下代码来完成Java服务端的V4签名计算。您也可以下载完整的Spring Boot示例工程upload_server.zip运用到您的项目当中。

  • 获取签名信息的工具类示例。

    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.HashMap;
    import java.util.Map;
    
    // 用于生成阿里云POST文件上传所需签名及相关配置信息的工具类
    public class GenerateSignature {
    
        // 生成签名及相关配置信息的主要方法
        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();
                    //  返回accessKeyId、secretAccessKey和securityToken
                    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);     //在当前时间增加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);        
    
            // 步骤1:创建policy
            String policy = "{" +
                    "\"expiration\": \"" + expirationTime + "\"," +
                    "\"conditions\": [" +
                    "{\"bucket\": \"cjl2\"}," +     //请替换为目标Bucket名称
                    "{\"x-oss-signature-version\": \"OSS4-HMAC-SHA256\"}," +
                    "{\"x-oss-credential\": \"" + accessKeyId + "/" + dtObj2 + "/cn-hangzhou/oss/aliyun_v4_request\"}," +
                    "{\"x-oss-security-token\": \"" + securityToken + "\"}," +
                    "{\"x-oss-date\": \"" + dtObj1 + "\"}" +
                    "]" +
                    "}";
    
            // 步骤2:构造待签名字符串(StringToSign)  
            String stringToSign = new String(Base64.encodeBase64(policy.getBytes()));
    
            // 步骤3:计算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");
    
            // 步骤4:计算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");  //指定签名的版本和算法,固定值为OSS4-HMAC-SHA256
            messageMap.put("policy", stringToSign);  //表单域
    
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(messageMap);
        }
    
    // 使用HMAC-SHA256算法计算给定密钥和数据的哈希值的静态方法
        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);
            }
        }
    }
  • 访问接口示例。

    package com.example.demo.controller;
    
    import com.example.demo.util.GenerateSignature;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class VxController {
    
        @GetMapping("/generate_signature")
        public String generate_signature() throws JsonProcessingException {
            GenerateSignature generateSignature = new GenerateSignature();
            return generateSignature.getSignature();
        }
    }

步骤二:配置微信小程序

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

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

      image

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

      说明

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

      image

  2. 在微信小程序端,使用从ECS服务端获取到的V4签名凭证信息,发送请求上传文件到OSS。完整示例工程及操作步骤如下。

    说明

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

    1. 下载小程序端示例工程uploadoss.zip并部署。

    2. 请编辑位于此路径下的 "uploadoss/pages/index/index.js"文件,完成小程序端上传文件相关配置。

      1. 自定义上传文件名称。

        image

      2. 填写ECS实例服务端接口地址。

        image

      3. 进入Bucket列表,选择目标Bucket后在Bucket内的左侧导航栏,单击概览,然后在访问端口区域复制Bucket域名。并将其填写到如图所示的标注位置。

        image

结果验证

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

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

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