[对接] pc端、小程序使用阿里云oss直传

170 阅读4分钟

1.1 为什么使用客户端直传?

前端直接将文件上传至oss,不经过网站服务器中转,可以减轻服务器压力,提高文件上传的速度。 image.png 实际使用:原先使用5M带宽的阿里云服务器,上传8M图片(高拍仪拍摄)需要花费近20s,非常慢,改用阿里云oss直传后,速度为6s左右,提升很大。

1.2 表单上传方式

流程:

  1. 客户端先请求获取签名和密钥(AccessKey)
  2. 再使用form-data方式上传文件

限制:上传文件大小不能超过5GB(超过考虑使用分片上传

优点:不依赖OSS SDK

缺点:

  1. 后端返回的签名和密钥是长期有效的,别人如果盗取了,就能往我们的oss随意存储数据
  2. 此方案不支持基于分片上传大文件、基于分片断点续传的场景

注意事项:

  1. 文件名添加uuid,防止覆盖(或请求头携带参数x-oss-forbid-overwrite: true)
  2. 最终上传到oss的图片地址通过拼接得到

(示例代码不再给出,文档已经很清楚了)

参考文档:表单上传方式OSS设置跨域资源共享CORS

1.3 STS上传方式

流程:

  1. 服务端使用STS SDK获取STS临时访问凭证
  2. 客户端请求拿到临时访问凭证
  3. 使用OSS SDK直接上传文件,后端返回文件的临时url(不影响前端展示,因为会更新)

优点:

  1. 安全
  2. 支持分片上传、分片断点续传

缺点:依赖OSS SDK

注意事项:

  1. 频繁地调用STS服务会引起限流,因此需要对STS临时凭证做缓存处理,并在有效期前刷新

  2. 后台跨域规则中要允许PUT方法,否则使用put上传文件时,会报跨域错误!

image.png

  1. (坑)put方法上传成功后,res.size为0!获取文件大小使用file.size,而不是返回的size

参考文档:服务端生成STS临时访问凭证的上传方式安装使用OSS SDK

具体代码:

先安装SDK开发包(源码):npm install ali-oss --save

/** STS临时凭证 */
let credentials = null

/**
 * 判断临时凭证是否到期。
 **/
function isCredentialsExpired(credentials) {
    if (!credentials) {
        return true
    }
    // credentials.expiration返回是秒
    const expireDate = new Date().getTime() + credentials.expiration * 1000
    const now = new Date().getTime()
    // 如果有效期不足十分钟,视为过期
    return expireDate - now <= 10 * 60 * 1000
}

/**
 * 阿里云STS上传方案
 * @param options
 *     file: 文件
 *     fileName: 文件名
 *     uploadDir: 上传目录
 */
function ossStsUpload(options) {
    let { file, fileName, uploadDir } = options || {}

    // 阿里云存储图片的目录(读取文件名时,需要再拆开读取)
    if (!uploadDir) {
        uploadDir = `${formatDate({ targetFormat: '{0}-{1}-{2}' })}`
    }

    if (!fileName) {
        fileName = `${uploadDir}/${uid()}_${file.name}`
    } else {
        fileName = `${uploadDir}/${fileName}`
    }

    return new Promise(async (resolve, reject) => {
        // 临时凭证过期时,才重新获取,减少对STS服务的调用
        if (isCredentialsExpired(credentials)) {
            const res = await API.getStsToken({})
            credentials = res.returnValue
        }

        // 初始化SDK
        const client = new OSS({
            bucket: credentials.bucketName,
            region: 'oss-cn-hangzhou',
            accessKeyId: credentials.accessKeyId,
            accessKeySecret: credentials.accessKeySecret,
            stsToken: credentials.securityToken,
        })

        try {
            // 调用SDK上传文件到OSS
            const uploadResult = await client.put(fileName, file)
            console.log('上传成功:', uploadResult)
            // const getResult = await client.get('object')
            // console.log('获取文件成功:', getResult)
            resolve(uploadResult)
        } catch (error) {
            console.error('发生错误:', error)
            reject(error)
        }
    })
}

1.4 微信小程序直传

因为微信小程序是基于微信的uploadFile接口上传文件,所以无法使用OSS SDK

但依旧支持STS上传方式,只是步骤变了,还需要客户端生成签名

参考文档:微信小程序直传实践

注意事项:

  1. 需要在小程序后台配置uploadFile和downloadFile的合法域名,为oss的Host地址

  2. 微信控制台访问oss资源403解决方法:

    (1)oss后台防盗链配置增加:servicewechat.com

    (2)小程序image组件、video组件、wx.previewMedia增加:referrerPolicy="origin"

具体代码:

import crypto from 'crypto-js';
import { Base64 } from 'js-base64';

/**
 * 微信小程序上传阿里云oss
 * filePath: 要上传的文件地址
 * fileSuffix: 要上传的文件地址后缀,默认png
 */
function uploadOss(filePath, fileSuffix = 'png') {
    return new Promise((resolve, reject) => {
        http.getData('/base/getOssStsToken', {}, (res) => {
            const data = res.returnValue;

            // 客户端获取STS临时访问凭证并生成签名
            function computeSignature(accessKeySecret, canonicalString) {
                return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret));
            }

            const date = new Date();
            // date.setHours(date.getHours() + 1);
            date.setMinutes(date.getMinutes() + 30);
            const policyText = {
                expiration: date.toISOString(), // 设置policy过期时间
                conditions: [
                    // 限制上传大小,目前是10MB
                    ['content-length-range', 0, 10 * 1024 * 1024],
                ],
            };

            async function getFormDataParams() {
                // const credentials = await axios.get('/getToken')
                const policy = Base64.encode(JSON.stringify(policyText)); // policy必须为base64的string。
                const signature = computeSignature(data.accessKeySecret, policy);
                const formData = {
                    // 上传目录为当前日期,并且采用当前的时间戳 + 150内的随机数来给图片命名
                    key:
                        commonFormatDate({ targetFormat: '{0}-{1}-{2}' }) +
                        '/' +
                        new Date().getTime() +
                        Math.floor(Math.random() * 150) +
                        '.' +
                        fileSuffix,
                    OSSAccessKeyId: data.accessKeyId,
                    signature,
                    policy,
                    'x-oss-security-token': data.securityToken,
                };
                return formData;
            }

            getFormDataParams().then((formData) => {
                const host = 'https://xxx-static.oss-cn-hangzhou.aliyuncs.com';
                wx.uploadFile({
                    url: host,
                    filePath,
                    name: 'file',
                    formData,
                    success: (res) => {
                        if (res.statusCode === 204) {
                            console.log('上传成功', res);
                            resolve(host + '/' + formData.key);
                        }
                    },
                    fail: (err) => {
                        console.log(err);
                        reject(err);
                    },
                });
            });
        });
    });
}

1.5 结束

2024/10/26:发布