前端直传cos之使用临时密钥&实现node获取临时密钥接口

背景

原来使用的cos是调用的node接口,但是由于公司node项目的网关限制了上传文件大小,然后的然后就由前端直传cos了(主要是还是自己动手丰衣足食);
但是呢!前端直传cos使用固定密钥是非常不安全的,所以使用node封装一个返回临时密钥的接口,然后前端调用临时密钥再上传cos~

具体实现

1. 实现node接口

  • 使用插件requestcrypto
  • 服务端使用固定密钥调用 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值是GetFederationTokenendpoint = 'sts.tencentcloudapi.com'就按照sts参考文件的来即可,不用再变了;
// 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 string
  json2str(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?.secretId
  const secretKey = options?.secretKey
  const proxy = options?.proxy || ''
  const region = options?.region || 'ap-beijing'
  const durationSeconds = options?.durationSeconds || options?.durationInSeconds || 1800
  const policy = options?.policy
  const 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-undef
  const 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 = name
    params.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.Response
    if (data) {
      if (data.Error) {
        callback(data.Error)
      } else {
        try {
          data.startTime = data.ExpiredTime - durationSeconds
          data = 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.com
      durationSeconds: 1800, // 密钥有效期
      // 放行判断相关参数
      bucket: "bucket", // 换成你的 bucket
      region: "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()
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?.result
                callback({
                    TmpSecretId: credentials.tmpSecretId,
                    TmpSecretKey: credentials.tmpSecretKey,
                    SecurityToken: credentials.sessionToken,
                    // 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
                    StartTime, // 时间戳,单位秒,如:1580000000
                    ExpiredTime, // 时间戳,单位秒,如: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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值