nestjs 阿里云服务端签名
1、将配置文件中的信息更换成自己的即可使用 服务端
下载包
npm install ali-oss npm install @alicloud/credentials
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、使用 客户端
下载包:
npm i ali-oss
主要代码:
// 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))); });
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;}// 获取ossApiconst 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 && (<divclassName="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]"><Imageheight={80}width={'auto'}src={item.url}key={index}fallback="data:image/png;base64,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"/>{!disabled && (<divclassName="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 ? (<Uploadaccept={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;