Bootstrap

uniapp+python使用临时签名上传腾讯云oss对象储存方案

概述

uniapp使用临时签名上传腾讯云oss对象储存方案,支持小程序、app、h5;
前端不依赖腾讯云SDK工具类;
后端使用python实现,需要安装qcloud-python-sts;
其中计算文件md5值使用了条件编译,因为每个环境获取ArrayBuffer方案不一样都不兼容;
 pip install qcloud-python-sts==3.1.6

那些踩过的坑🕳

  • 官方方案小程序SDK,但是小程序SDK在APP环境由于无法获取file://类型地址的文件;
  • 官方方案JS-SDK,此方案由于要使用file对象然而APP端无法使用Blob工具类;

uniapp实现

import SparkMD5 from "spark-md5"; //md5工具类 使用npm安装
import {
	api_getBucketAndRegionSelf
} from "@/api/common";//此处是后端拿临时密钥等信息的
import {
	OSS_BASE_URL
} from '../config';//此处获取到的是自定义的域名



/**
 * 上传文件 路径为 年/月/日/keypath/fileMD5.xx
 * @param {string} keyPath 文件分路径(可留空) 例:users/user1
 * @param {string} file 文件路径
 * @returns {Promise<string|null>} 文件上传后的URL
 */
async function putObjectAutoPath(keyPath, file) {
	console.log("getMD5FileName")
	try {

		console.log("getMD5FileName")
		const md5FileName = await getMD5FileName(file);
		const datePath = getDatePath();
		const uploadPath = `${datePath}${keyPath.trim() ? `${keyPath.trim()}/` : ''}${md5FileName}`;
		console.log("上传路径为:" + uploadPath);
		console.log("图片路径=>" + file);

		const res = await api_getBucketAndRegionSelf(uploadPath);

		console.log(res)
		const formData = {
			key: res.data.cosKey,
			policy: res.data.policy, // 这个传 policy 的 base64 字符串
			success_action_status: 200,
			'q-sign-algorithm': res.data.qSignAlgorithm,
			'q-ak': res.data.qAk,
			'q-key-time': res.data.qKeyTime,
			'q-signature': res.data.qSignature,
			'x-cos-security-token': res.data.securityToken
		};

		const uploadResult = await uploadFile('https://' + res.data.cosHost, file, formData);
		console.log('上传成功:', uploadResult);
		return OSS_BASE_URL + res.data.cosKey;
	} catch (error) {
		console.error('上传失败:', error);
		throw error;
	}
}

/**
 * 生成文件夹路径 [时间命名]
 * @returns {string} keyPath
 */
function getDatePath() {
	const date = new Date();
	const year = date.getFullYear();
	const month = String(date.getMonth() + 1).padStart(2, "0");
	const day = String(date.getDate()).padStart(2, "0");
	return `/${year}/${month}/${day}/`;
}

/**
 * 计算文件的 MD5 哈希值
 * @param {File|string} file 文件对象或文件路径
 * @returns {Promise<string>} MD5 哈希值
 */
function calculateMD5(file) {
	return new Promise((resolve, reject) => {
		// 在 Web 环境下使用 FileReader
		//#ifdef H5
		console.log("执行md5值计算H5", file);
		const xhr = new XMLHttpRequest();
		xhr.open('GET', file, true);
		xhr.responseType = 'blob';
		xhr.onload = function() {
			if (xhr.status === 200) {
				const blob = xhr.response;
				const reader = new FileReader();
				reader.onload = (e) => {
					const binary = e.target.result;
					const spark = new SparkMD5.ArrayBuffer();
					spark.append(binary);
					resolve(spark.end());
				};
				reader.onerror = reject;
				reader.readAsArrayBuffer(blob);
			} else {
				reject(new Error('Failed to fetch blob'));
			}
		};
		xhr.onerror = reject;
		xhr.send();
		//#endif

		//#ifndef H5
		//#ifndef APP-PLUS
		console.log("执行md5值计算MP");
		const fs = uni.getFileSystemManager();
		fs.readFile({
			filePath: file, // 文件路径
			encoding: 'base64',
			success: (res) => {
				const binary = uni.base64ToArrayBuffer(res.data); // 将 base64 转换为 ArrayBuffer
				const spark = new SparkMD5.ArrayBuffer();
				spark.append(binary);
				resolve(spark.end());
			},
			fail: reject,
		});
		//#endif
		//#endif

		//#ifdef APP-PLUS
		console.log("执行md5值计算APP");
		plus.io.resolveLocalFileSystemURL(file, (entry) => {

			entry.file((fileObj) => {
				const reader = new plus.io.FileReader();
				reader.readAsDataURL(file);

				reader.onloadend = (evt) => {

					const binary = uni.base64ToArrayBuffer(evt.target
						.result); // 将 base64 转换为 ArrayBuffer
					const spark = new SparkMD5.ArrayBuffer();
					spark.append(binary);
					resolve(spark.end());
				};
				reader.onerror = reject;
			});
		}, reject);
		//#endif
	});
}

/**
 * 获取文件MD5名称
 * @param {string} file 文件路径
 * @returns {Promise<string>} MD5文件名
 */
async function getMD5FileName(file) {
	const md5 = await calculateMD5(file);
	console.log(md5)
	return;
	const fileType = file.substring(file.lastIndexOf("."));
	return `${md5}${fileType}`;
}

/**
 * 文件上传
 * @param {Object} url
 * @param {Object} filePath
 * @param {Object} formData
 * @returns {Promise<string|null>} 
 */
function uploadFile(url, filePath, formData) {
	return new Promise((resolve, reject) => {
		uni.uploadFile({
			url: url,
			filePath: filePath,
			name: 'file',
			formData: formData,
			success: (res) => {
				if (res.statusCode === 200) {
					resolve(res);
				} else {
					reject(new Error(`上传失败,状态码:${res.statusCode}, 响应信息:${res.data}`));
				}
			},
			error: (err) => {
				console.log("图片上传失败=》" + res)
				reject(err);
			},
		});
	});
}
export {
	putObjectAutoPath
};

python实现的

#!/usr/bin/env python
# coding=utf-8
import json

from sts.sts import Sts
import hashlib
import hmac
import base64
import time
from datetime import datetime, timedelta



#腾讯云 secret_id
secret_id = ''
#腾讯云 secret_key
secret_key = ''
#bucketId 储存桶ID
bucket = ''
#存储桶所在地域
region = ''

def get_temporary_credential():

    """
        获取临时密钥
    :return:
    """

    config = {
        # 请求URL,域名部分必须和domain保持一致
        # 使用外网域名时:https://sts.tencentcloudapi.com/
        # 使用内网域名时:https://sts.internal.tencentcloudapi.com/
        # 'url': 'https://sts.tencentcloudapi.com/',
        # # 域名,非必须,默认为 sts.tencentcloudapi.com
        # # 内网域名:sts.internal.tencentcloudapi.com
        # 'domain': 'sts.tencentcloudapi.com',
        # 临时密钥有效时长,单位是秒
        'duration_seconds': 1800,
        'secret_id': secret_id,
        # 固定密钥
        'secret_key': secret_key,
        # 设置网络代理
        # 'proxy': {
        #     'http': 'xx',
        #     'https': 'xx'
        # },
        # 换成你的 bucket
        'bucket': bucket,
        # 换成 bucket 所在地区
        'region': region,
        # 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
        # 例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
        'allow_prefix': ['*'],
        # 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
        'allow_actions': [
            # 简单上传
            'name/cos:PutObject',
            'name/cos:PostObject',
            # 分片上传
            'name/cos:InitiateMultipartUpload',
            'name/cos:ListMultipartUploads',
            'name/cos:ListParts',
            'name/cos:UploadPart',
            'name/cos:CompleteMultipartUpload'
        ],
        # # 临时密钥生效条件,关于condition的详细设置规则和COS支持的condition类型可以参考 https://cloud.tencent.com/document/product/436/71306
        # "condition": {
        #     "ip_equal":{
        #         "qcs:ip":[
        #             "10.217.182.3/24",
        #             "111.21.33.72/24",
        #         ]
        #     }
        # }

    }
    try:
        sts = Sts(config)
        response = sts.get_credential()
        print(response)



        # 添加新的属性
        response['bucket'] = bucket
        response['region'] = region

        return response
    except Exception as e:
        raise Exception("腾讯OSS临时密钥获取异常!")


def get_bucketAndRegion():
    """
        获取bucket 桶id 和region地域
    :return:
    """
    data = {
        "bucket": bucket,
        "region": region
    }
    return data



def get_temporary_credential_self_upload(keyPath):
    """
         获取腾讯云oss凭证 适用于POST上传请求【不依赖腾讯SDK】
    """

    #获取临时签名
    credentials_data = get_temporary_credential().get("credentials")
    tmp_secret_id = credentials_data.get("tmpSecretId")
    tmp_secret_key = credentials_data.get("tmpSecretKey")
    session_token = credentials_data.get("sessionToken")


    # 开始计算凭证
    cos_host = f"{bucket}.cos.{region}.myqcloud.com"
    cos_key = keyPath
    now = int(time.time())
    exp = now + 900
    q_key_time = f"{now};{exp}"
    q_sign_algorithm = 'sha1'

    # 生成上传要用的 policy
    policy = {
        'expiration': (datetime.utcfromtimestamp(exp)).isoformat() + 'Z',
        'conditions': [
            {'q-sign-algorithm': q_sign_algorithm},
            {'q-ak': tmp_secret_id},
            {'q-sign-time': q_key_time},
            {'bucket': bucket},
            {'key': cos_key},
        ]
    }
    policy_encoded = base64.b64encode(json.dumps(policy).encode()).decode()
    # 步骤一:生成 SignKey
    sign_key = hmac.new(tmp_secret_key.encode(), q_key_time.encode(), hashlib.sha1).hexdigest()

    # 步骤二:生成 StringToSign
    string_to_sign = hashlib.sha1(json.dumps(policy).encode()).hexdigest()

    # 步骤三:生成 Signature
    q_signature = hmac.new(sign_key.encode(), string_to_sign.encode(), hashlib.sha1).hexdigest()
    return {
            'cosHost': cos_host,
            'cosKey': cos_key,
            'policy': policy_encoded,
            'qSignAlgorithm': q_sign_algorithm,
            'qAk': tmp_secret_id,
            'qKeyTime': q_key_time,
            'qSignature': q_signature,
            'securityToken': session_token  # 如果 SecretId、SecretKey 是临时密钥,要返回对应的 sessionToken 的值
        }
;