背景
原来使用的cos是调用的node接口,但是由于公司node项目的网关限制了上传文件大小,然后的然后就由前端直传cos了(主要是还是自己动手丰衣足食);
但是呢!前端直传cos使用固定密钥是非常不安全的,所以使用node封装一个返回临时密钥的接口,然后前端调用临时密钥再上传cos~
具体实现
1. 实现node接口
- 使用插件
request
、crypto
- 服务端使用固定密钥调用 STS 服务申请临时密钥(具体内容请参见文底参考文档)
- STS服务接入(参考:https://github.com/tencentyun/qcloud-cos-sts-sdk/blob/master/nodejs/sdk/sts.js)
- sts文件注意事项:
- request引用使用报错,将引入改成
import * as request from 'request'
即可; - 内部
params
参数按照sts参考文件都要加上,否则运行的时候会报错缺少参数; - action值是
GetFederationToken
,endpoint = 'sts.tencentcloudapi.com'
就按照sts参考文件的来即可,不用再变了;
- request引用使用报错,将引入改成
// sts.ts 文件
/* eslint-disable */
import * as request from 'request'
const crypto = require('crypto')
const StsUrl = 'https://{host}/'const util = {// 获取随机数getRandom(min, max) {return Math.round(Math.random() * (max - min) + min)},// obj 转 query stringjson2str(obj, $notEncode = '') {const arr: any = []Object.keys(obj).sort().forEach(item => {const val = obj[item] || ''arr.push(`${item}=${$notEncode ? encodeURIComponent(val) : val}`)})return arr.join('&')},// 计算签名getSignature(opt, key, method, stsDomain) {const formatString = `${method + stsDomain}/?${util.json2str(opt)}`const hmac = crypto.createHmac('sha1', key)const sign = hmac.update(Buffer.from(formatString, 'utf8')).digest('base64')return sign},// v2接口的key首字母小写,v3改成大写,此处做了向下兼容backwardCompat(data) {const compat:any = {}for (const key in data) {if (typeof data[key] === 'object') {compat[this.lowerFirstLetter(key)] = this.backwardCompat(data[key])} else if (key === 'Token') {compat.sessionToken = data[key]} else {compat[this.lowerFirstLetter(key)] = data[key]}}return compat},lowerFirstLetter(source) {return source.charAt(0).toLowerCase() + source.slice(1)},
}// 拼接获取临时密钥的参数
const getTempCredential = function (options, callback) {if (options?.durationInSeconds !== undefined) {console.warn('warning: durationInSeconds has been deprecated, Please use durationSeconds ).')}const secretId = options?.secretIdconst secretKey = options?.secretKeyconst proxy = options?.proxy || ''const region = options?.region || 'ap-beijing'const durationSeconds = options?.durationSeconds || options?.durationInSeconds || 1800const policy = options?.policyconst endpoint = 'sts.tencentcloudapi.com'const policyStr = JSON.stringify(policy)const action = options?.action || 'GetFederationToken'const nonce = util.getRandom(10000, 20000)const timestamp = parseInt(`${+new Date() / 1000}`) // eslint-disable-line no-undefconst method = 'POST'const name = 'cos-sts-nodejs' // 临时会话名称const params: any = {SecretId: secretId,Timestamp: timestamp,Nonce: nonce,Action: action,DurationSeconds: durationSeconds,Version: '2018-08-13',Region: region,Policy: encodeURIComponent(policyStr),}if (action === 'AssumeRole') {params.RoleSessionName = nameparams.RoleArn = options?.roleArn} else {params.Name = name}params.Signature = util.getSignature(params, secretKey, method, endpoint)const opt = {method,url: StsUrl.replace('{host}', endpoint),strictSSL: false,json: true,form: params,headers: {Host: endpoint,},proxy,}request(opt, (err, response, body) => {let data = body.Responseif (data) {if (data.Error) {callback(data.Error)} else {try {data.startTime = data.ExpiredTime - durationSecondsdata = util.backwardCompat(data)callback(null, data)} catch (e) {callback(new Error(`Parse Response Error: ${JSON.stringify(data)}`))}}} else {callback(err || body)}})
}// 获取联合身份临时访问凭证 GetFederationToken
const getCredential = (opt, callback) => {Object.assign(opt, { action: 'GetFederationToken' })if (callback) return getTempCredential(opt, callback)return new Promise((resolve, reject) => {getTempCredential(opt, (err, data) => {err ? reject(err) : resolve(data)})})
}}const STS = {getCredential,
}
export default STS
- node调用sts接口调用固定密钥,生成临时密钥接口
- 入参: 固定的密钥
- 返回数据 临时token、临时密钥
async getTempCosKeyId() {const secretKeyId = await getSecretKeyId();// 配置参数const config = {secretId: secretKeyId.secretId, // 固定密钥secretKey: secretKeyId.secretKey, // 固定密钥proxy: "",host: "sts.tencentcloudapi.com", // 域名,非必须,默认为 sts.tencentcloudapi.comdurationSeconds: 1800, // 密钥有效期// 放行判断相关参数bucket: "bucket", // 换成你的 bucketregion: "region", // 换成 bucket 所在地区allowPrefix: "/web", // 上传文件前缀,可自定义为惯用前缀,};const bucket = config.bucket || "";const shortBucketName = bucket.slice(0, bucket.lastIndexOf("-"));const appId = bucket.slice(1 + bucket.lastIndexOf("-"));const policy = {version: "2.0",statement: [{action: [// 简单上传"name/cos:PutObject","name/cos:PostObject",// 分片上传"name/cos:InitiateMultipartUpload","name/cos:ListMultipartUploads","name/cos:ListParts","name/cos:UploadPart","name/cos:CompleteMultipartUpload",],effect: "allow",principal: { qcs: ["*"] },resource: [`qcs::cos:${config.region}:uid/${appId}:prefix//${appId}/${shortBucketName}/${config.allowPrefix}`,],},],};// 返回接口return new Promise((resolve, reject) => {STS.getCredential({secretId: config.secretId,secretKey: config.secretKey,proxy: config.proxy,policy,durationSeconds: config.durationSeconds,},(err, credential) => {if (!err) {resolve(credential);console.log(err || credential);} else {reject(err);}});}).catch((error) => error);}
- 关于前缀配置,
allowPrefix: "/web"
上传文件前缀,可自定义为惯用前缀, 上传文件的时候必须用到否则接口403报错
2. 前端封装cos直传组件
- 使用插件
cos-js-sdk-v5
- 将cos里面的密钥
{SecretId:,SecretKey}
换成获取临时密钥getAuthorization:(op,callback)=>{//接口获取临时密钥,执行callback}
- 上传组件封装
- 大于20m的进行分片上传
cos.sliceUploadFile()
,其他的就直接走上传cos.putObject()
- 大于20m的进行分片上传
import { UPLOAD_BUCKET, UPLOAD_REGION, UPLOAD_PREFIX, } from '@/utils/globalData';
import { queryGetTempCosKeyId } from '@/services/upload' // 获取临时密钥接口
const COS = require('cos-js-sdk-v5');
/*** @param {object} option*/
export default function uploadCos(option) {const cos = new COS({// getAuthorization 必选参数getAuthorization: function (options, callback) {// 异步获取临时密钥queryGetTempCosKeyId().then(res => {const { credentials, expiredTime: ExpiredTime, startTime: StartTime } = res?.data?.resultcallback({TmpSecretId: credentials.tmpSecretId,TmpSecretKey: credentials.tmpSecretKey,SecurityToken: credentials.sessionToken,// 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误StartTime, // 时间戳,单位秒,如:1580000000ExpiredTime, // 时间戳,单位秒,如:1580000000});})}});const Bucket_Region_Config = {Bucket: UPLOAD_BUCKET,Region: UPLOAD_REGION,};const errFn = (err, data) => {if (err) {option.onError(err);} else {option.onSuccess(data);}}const progressFn = (progressData) => {if (!done) {option.onProgress(progressData);if (progressData.percent >= 1) {done = true;}}}const { file = {}, Prefix = UPLOAD_PREFIX } = option;let done = false;const timestamp = new Date().getTime();const newFileName = `${timestamp}_${file.name}`;if (file.size > 1024 * 1024 * 20) { // 大于20m走分片上传cos.sliceUploadFile({...Bucket_Region_Config,Key: (Prefix || '') + newFileName,Body: file,onProgress: (progressData) => progressFn(progressData)},(err, data) => errFn(err, data));} else {cos.putObject({...Bucket_Region_Config,Key: (Prefix || '') + newFileName,Body: file,onProgress: (progressData) => progressFn(progressData)},(err, data) => errFn(err, data));}return false;
}
3. 组件使用
- 使用uploadCos方法上传文件,传入参数file,调用
onSuccess
成功方法获取文件,调用onError
方法获取失败原因
import uploadCos from '@/utils/uploadCos'
const uploadAction = (val) => {uploadCos({file: val.file,onSuccess: (completeData) => {setUploadVal(val => {console.log(`上传完成,文件为${completeData?.Location}`);},onError: (err) => {message.error(`文件上传失败,请稍后重试!,${err}`)},});
}
参数
node获取临时密钥接口:https://github.com/tencentyun/qcloud-cos-sts-sdk/blob/master/nodejs/demo/sts-server.js
前端使用临时密钥:https://cloud.tencent.com/document/product/436/11459
cos文档:https://cloud.tencent.com/document/product/436/14048