nestjs 阿里云服务端签名

1、将配置文件中的信息更换成自己的即可使用 服务端

下载包

复制代码
npm install ali-oss
npm install @alicloud/credentials
TypeScript 复制代码
import { Injectable } from "@nestjs/common";
import { ResultData } from "src/utils/result-data";
import { STS } from 'ali-oss';
import OSS from 'ali-oss';
import { getCredential } from 'ali-oss/lib/common/signUtils'
import { getStandardRegion } from 'ali-oss/lib/common/utils/getStandardRegion'
import { policy2Str } from 'ali-oss/lib/common/utils/policy2Str'
import { ConfigService } from "@nestjs/config";

@Injectable()
export class UploadService {
    constructor(
        private readonly configService: ConfigService,
    ) { }
    async generateSignature() {
        // 初始化STS客户端
        let sts = new STS({
            accessKeyId: this.configService.get('oss.accessKeyId'),
            accessKeySecret: this.configService.get('oss.accessKeySecret')
        });
        // 调用assumeRole接口获取STS临时访问凭证
        const result = await sts.assumeRole(this.configService.get('oss.assumeRole'), '', '3600');
        const accessKeyId = result.credentials.AccessKeyId;
        const accessKeySecret = result.credentials.AccessKeySecret;
        const securityToken = result.credentials.SecurityToken;
        const client = new OSS({
            region: this.configService.get('oss.region'),
            accessKeyId,
            accessKeySecret,
            stsToken: securityToken,
            bucket: this.configService.get('oss.bucket'),
            refreshSTSTokenInterval: 0,
            refreshSTSToken: async () => {
                const { accessKeyId, accessKeySecret, securityToken } = await client.getCredential();
                return { accessKeyId, accessKeySecret, stsToken: securityToken };
            },
        });
        const formData = new Map();
        const date = new Date();
        const expirationDate = new Date(date);
        expirationDate.setMinutes(date.getMinutes() + 10);
        function padTo2Digits(num) {
            return num.toString().padStart(2, '0');
        }
        function formatDateToUTC(date) {
            return (
                date.getUTCFullYear() +
                padTo2Digits(date.getUTCMonth() + 1) +
                padTo2Digits(date.getUTCDate()) +
                'T' +
                padTo2Digits(date.getUTCHours()) +
                padTo2Digits(date.getUTCMinutes()) +
                padTo2Digits(date.getUTCSeconds()) +
                'Z'
            );
        }
        const formattedDate = formatDateToUTC(expirationDate);
        // 生成x-oss-credential并设置表单数据
        const credential = getCredential(formattedDate.split('T')[0], getStandardRegion(client.options.region), client.options.accessKeyId);
        formData.set('x_oss_date', formattedDate);
        formData.set('x_oss_credential', credential);
        formData.set('x_oss_signature_version', 'OSS4-HMAC-SHA256');

        // 创建policy
        // 示例policy表单域只列举必填字段
        const policy: { expiration: string, conditions: any[] } = {
            expiration: expirationDate.toISOString(),
            conditions: [
                { 'bucket': this.configService.get('oss.bucket') },
                { 'x-oss-credential': credential },
                { 'x-oss-signature-version': 'OSS4-HMAC-SHA256' },
                { 'x-oss-date': formattedDate },
            ],
        };
        // 如果存在STS Token,添加到策略和表单数据中
        if (client.options.stsToken) {
            policy.conditions.push({ 'x-oss-security-token': client.options.stsToken });
            formData.set('security_token', client.options.stsToken);
        }

        // 生成签名并设置表单数据
        const signature = client.signPostObjectPolicyV4(policy, date);
        formData.set('policy', Buffer.from(policy2Str(policy), 'utf8').toString('base64'));
        formData.set('signature', signature);

        // 返回表单数据
        const data = {
            host: `http://${client.options.bucket}.oss-${client.options.region}.aliyuncs.com`,
            policy: Buffer.from(policy2Str(policy), 'utf8').toString('base64'),
            x_oss_signature_version: 'OSS4-HMAC-SHA256',
            x_oss_credential: credential,
            x_oss_date: formattedDate,
            signature: signature,
            accessKeyId: client.options.accessKeyId,
            accessKeySecret: client.options.accessKeySecret,
            region: client.options.region,
            bucket: client.options.bucket,
            dir: this.configService.get('oss.dir'), // 指定上传到OSS的文件前缀
            stsToken: client.options.stsToken
        };
        return ResultData.ok(data)
    }
}

2、使用 客户端

下载包:

TypeScript 复制代码
npm i ali-oss

主要代码:

TypeScript 复制代码
// OSS实例
const getOssClient = (ossInfo: any) => {
    const client = new OSS({
        region: ossInfo.region,
        authorizationV4: true,
        accessKeyId: ossInfo.accesskeyid,
        accessKeySecret: ossInfo.accesskeysecret,
        stsToken: ossInfo.security_token,
        bucket: ossInfo.bucket,
    });
    return client;
};




// 普通上传
await ossClient.put(`${dir}/${fileName}`, file)




//分包上传
ossClient.multipartUpload(`${dir}/${fileName}`, file, {
    parallel: 4,
    partSize: 1024 * 1024 * 10,
    progress: function (p: any, cpt: any) {
        console.log(p, cpt);
    },
}).then(function (res: any) {
    setFileCount(fileCount - 1);
    for (let i in res.res.requestUrls) {
        fileListRef.current.push({
            url: res.res.requestUrls[i],
            courseId: '',
        });
    }
    updateFileString?.(JSON.parse(JSON.stringify(fileListRef.current)));
});
TypeScript 复制代码
import { getOssToken } from '@/services/system';
// @ts-ignore
import OSS from 'ali-oss';
import { Button, Image, message, Spin, Upload } from 'antd';
import { useEffect, useRef } from 'react';
import { useImmer } from 'use-immer';
const UploadFile = ({
    maxCount = 1,
    fileList,
    multiple = true,
    updateFileString,
    children,
    accept = '*',
    disabled = false,
    subPackageSize = 1024 * 1024 * 100, //分包大小
}: {
    maxCount?: number;
    fileList: { url: string; courseId: string }[];
    updateFileString?: Function;
    children?: any;
    multiple?: boolean;
    accept?: string;
    disabled?: boolean;
    subPackageSize?: number;
}) => {
    const fileListRef = useRef<Array<{ url: string; courseId: string }>>([]);
    const [update, setUpdate] = useImmer(false);
    useEffect(() => {
        setUpdate(!update);
        fileListRef.current = JSON.parse(JSON.stringify(fileList));
    }, [fileList]);

    const [fileCount, setFileCount] = useImmer(0);

    useEffect(() => {
        if (fileCount > 0) {
            message.loading({
                content: '上传中...',
                duration: 0
            });
        } else {
            message.destroy()

        }
    }, [fileCount]);
    function getFileBaseNameAndExtension(file: File) {
        // 确保传入的是 File 对象或具有 name 属性的对象
        if (!file || typeof file.name === 'undefined') {
            throw new Error('无效的文件对象');
        }

        const fullName = file.name;
        const lastDotIndex = fullName.lastIndexOf('.');

        let fileNameWithoutExt;
        let extension;

        if (lastDotIndex === -1) {
            // 文件名中没有点
            fileNameWithoutExt = fullName;
            extension = ''; // 或者可以设为 null
        } else {
            // 截取点之前的部分作为文件名
            fileNameWithoutExt = fullName.substring(0, lastDotIndex);
            // 截取点及之后的部分作为扩展名(包含点)
            extension = fullName.substring(lastDotIndex); // 例如 ".jpg", ".pdf"
            // 如果你希望 extension 不包含点,使用: extension = fullName.substring(lastDotIndex + 1);
        }

        return {
            fileName: fileNameWithoutExt + '_flyco_' + new Date().getTime(), // 不带后缀的文件名
            extension: extension, // 后缀名(包含点)
        };
    }

    function parseOSSFileName(fileName: string) {
        fileName = decodeURIComponent(fileName.split('/')[fileName.split('/').length - 1]);
        // 1. 找到最后一个点的位置来分离扩展名
        const lastDotIndex = fileName.lastIndexOf('.');

        // 2. 如果有后缀名
        if (lastDotIndex !== -1) {
            const extension = fileName.substring(lastDotIndex); // 包含点,如 ".docx"
            const nameWithoutExt = fileName.substring(0, lastDotIndex); // 去掉后缀的部分

            // 3. 使用正则匹配 _flyco_后跟纯数字 的模式
            const match = nameWithoutExt.match(/^(.+?)_flyco_\d+$/);

            // 4. 如果匹配成功,返回 捕获的原始名 + 后缀;否则返回原文件名
            if (match) {
                return match[1] + extension; // 原始名 + 后缀
            } else {
                return match + extension;
            }
        }
        // 5. 如果没有后缀 或 不符合规则,直接返回原文件名
        return fileName;
    }

    // 获取ossApi
    const getOssTokenApi = (file: any) => {
        getOssToken().then(async (res) => {
            if (res.code == 200) {
                const OssInit = getOssClient(res.data);
                if (file.size > subPackageSize) {
                    updateSubPackageFile(res.data.dir, OssInit, file);
                } else {
                    uploadPackageFile(res.data.dir, OssInit, file);
                }
            }
        });
    };
    // OSS实例
    const getOssClient = (ossInfo: any) => {
        const client = new OSS({
            region: ossInfo.region,
            authorizationV4: true,
            accessKeyId: ossInfo.accessKeyId,
            accessKeySecret: ossInfo.accessKeySecret,
            stsToken: ossInfo.stsToken,
            bucket: ossInfo.bucket,
        });
        return client;
    };

    // 分包上传
    const updateSubPackageFile = (dir: string, ossClient: any, file: any) => {
        const fileBaseNameAndExtension = getFileBaseNameAndExtension(file);
        const fileName = fileBaseNameAndExtension.fileName + fileBaseNameAndExtension.extension;

        ossClient
            .multipartUpload(`${dir}/${fileName}`, file, {
                parallel: 4,
                partSize: 1024 * 1024 * 10,
                progress: function (p: any, cpt: any) {
                    console.log(p, cpt);
                },
            })
            .then(function (res: any) {
                setFileCount(fileCount - 1);
                for (let i in res.res.requestUrls) {
                    fileListRef.current.push({
                        url: res.res.requestUrls[i],
                        courseId: '',
                    });
                }
                updateFileString?.(JSON.parse(JSON.stringify(fileListRef.current)));
            });
    };
    // 普通上传
    const uploadPackageFile = async (dir: string, ossClient: any, file: any) => {
        const fileBaseNameAndExtension = getFileBaseNameAndExtension(file);
        const fileName = fileBaseNameAndExtension.fileName + fileBaseNameAndExtension.extension;
        const result = await ossClient.put(`${dir}/${fileName}`, file);
        setFileCount(fileCount - 1);
        fileListRef.current.push({
            url: result.url,
            courseId: '',
        });
        updateFileString?.(JSON.parse(JSON.stringify(fileListRef.current)));
    };

    return (
        <div className="flex items-center flex-wrap">
            {fileListRef.current.map((item: { url: string; courseId: string }, index: number) => {
                const fileName = parseOSSFileName(item.url);
                if (accept != 'image/*') {
                    return (
                        <div key={index} className="text-mainColor ml-2 mb-2 mr-3 border-b-[2px] border-t-[0]  border-x-[0] border-mainColor border-solid flex-wrap flex max-w-[200px] cursor-pointer">
                            <div className="max-w-[150px] text-ellipsis">
                                <a target="_blank" className="max-w-[150px] text-ellipsis inline-block overflow-hidden text-nowrap" href={item.url}>
                                    {fileName}
                                </a>
                            </div>
                            {!disabled && (
                                <div
                                    className="iconfont icon-shanchu ml-2 cursor-pointer"
                                    onClick={() => {
                                        fileListRef.current.splice(index, 1);
                                        updateFileString?.(JSON.parse(JSON.stringify(fileListRef.current)));
                                    }}
                                ></div>
                            )}
                        </div>
                    );
                } else {
                    return (
                        <div className="w-[80px] h-[80px] text-center relative ml-2 border-dashed border-[#ccc] border-[1px]">
                            <Image
                                height={80}
                                width={'auto'}
                                src={item.url}
                                key={index}
                                fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=="
                            />
                            {!disabled && (
                                <div
                                    className="iconfont icon-shanchu text-[red] cursor-pointer ml-2 absolute right-0 top-0 cursor-pointer"
                                    onClick={() => {
                                        fileListRef.current.splice(index, 1);
                                        updateFileString?.(JSON.parse(JSON.stringify(fileListRef.current)));
                                    }}
                                ></div>
                            )}
                        </div>
                    );
                }
            })}
            {fileListRef.current.length < maxCount && !disabled ? (
                <Upload
                    accept={accept ? accept : '*'}
                    fileList={[]}
                    customRequest={(e: any) => {
                        setFileCount(fileCount + 1);
                        getOssTokenApi(e.file);
                    }}
                    multiple={multiple}
                >
                    <div className=" ml-3">{children ? children : <Button type="primary">上传附件</Button>}</div>
                </Upload>
            ) : (
                ''
            )}
        </div>
    );
};

export default UploadFile;
相关推荐
寻星探路3 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
盟接之桥6 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端