Front-end direct transmission of cos using temporary key & implementing node to obtain temporary key interface

Background

The originally used cos is the node interface called, but because the gateway of the company’s node project limits the size of the uploaded file, then the front end directly uploads the cos (mainly to do it yourself);
but! It is very unsafe to use a fixed key to directly upload cos from the front end, so use node to encapsulate an interface that returns a temporary key, and then the front end calls the temporary key to upload cos~

Concrete implementation

1. Implement node interface

  • Use plugins request, crypto
  • The server uses the fixed key to call the STS service to apply for a temporary key (see the reference document at the bottom of the article for details)
  • STS service access (reference: https://github.com/tencentyun/qcloud-cos-sts-sdk/blob/master/nodejs/sdk/sts.js)
  • Notes on sts files:
    • The request reference is used to report an error, and the introduction can be changed to import * as request from 'request';
    • The internal params parameters must be added according to the sts reference file, otherwise an error will be reported for missing parameters when running;
    • The action value is GetFederationToken, endpoint = 'sts.tencentcloudapi.com' just follow the sts reference file, no need to change;
// sts.ts file
/* eslint-disable */
import * as request from 'request'
const crypto = require('crypto')
const StsUrl = 'https://{host}/'

const util = {<!-- -->
  // get random number
  getRandom(min, max) {<!-- -->
    return Math. round(Math. random() * (max - min) + min)
  },
  // obj to 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(' & amp;')
  },
  // calculate the signature
  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
  },
  // The first letter of the key of the v2 interface is lowercase, v3 is changed to uppercase, here is backward compatibility
  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)
  },
}

// Concatenate the parameters to obtain the temporary key
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' // temporary session name
  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)
    }
  })
}

// Obtain federation temporary access credentials 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 calls the sts interface to call the fixed key to generate a temporary key interface
    • Input parameter: fixed key
    • Return data temporary token, temporary key
async getTempCosKeyId() {<!-- -->
    const secretKeyId = await getSecretKeyId();
    // configuration parameters
    const config = {<!-- -->
      secretId: secretKeyId.secretId, // fixed key
      secretKey: secretKeyId.secretKey, // fixed key
      proxy: "",
      host: "sts.tencentcloudapi.com", // domain name, optional, default is sts.tencentcloudapi.com
      durationSeconds: 1800, // key validity period
      // release judgment related parameters
      bucket: "bucket", // replace it with your bucket
      region: "region", // change to the region where the bucket is located
      allowPrefix: "/web", // upload file prefix, which can be customized as customary prefix,
    };
    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: [
            // simple upload
            "name/cos:PutObject",
            "name/cos:PostObject",
            // Multipart upload
            "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 interface
    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);
  }
  • Regarding the prefix configuration, allowPrefix: "/web" uploads the file prefix, which can be customized as a customary prefix, which must be used when uploading files, otherwise the interface will report an error of 403

2. Front-end packaging cos direct transmission components

  • Use the plugin cos-js-sdk-v5
  • Change the key {SecretId:,SecretKey} in cos to obtain a temporary key getAuthorization:(op,callback)=>{//Interface to obtain a temporary key, execute callback}
  • Upload component package
    • Upload cos.sliceUploadFile() in slices larger than 20m, and upload cos.putObject() directly for others
import {<!-- --> UPLOAD_BUCKET, UPLOAD_REGION, UPLOAD_PREFIX, } from '@/utils/globalData';
import {<!-- --> queryGetTempCosKeyId } from '@/services/upload' // Get temporary key interface
const COS = require('cos-js-sdk-v5');
/**
 * @param {object} option
 */
export default function uploadCos(option) {<!-- -->
    const cos = new COS({<!-- -->
        // getAuthorization required parameter
        getAuthorization: function (options, callback) {<!-- -->
            // Get the temporary key asynchronously
            queryGetTempCosKeyId().then(res => {<!-- -->
                const {<!-- --> credentials, expiredTime: ExpiredTime, startTime: StartTime } = res?.data?.result
                callback({<!-- -->
                    TmpSecretId: credentials.tmpSecretId,
                    TmpSecretKey: credentials.tmpSecretKey,
                    SecurityToken: credentials.sessionToken,
                    // It is recommended to return the server time as the start time of the signature, so as to avoid excessive deviation of the local time of the user's browser and cause signature errors
                    StartTime, // timestamp, in seconds, such as: 1580000000
                    ExpiredTime, // time stamp in seconds, such as: 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) {<!-- --> // upload in parts larger than 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. Component usage

  • Use the uploadCos method to upload files, pass in the parameter file, call the onSuccess method to obtain the file, and call the onError method to obtain the failure reason
import uploadCos from '@/utils/uploadCos'
const uploadAction = (val) => {<!-- -->
 uploadCos({<!-- -->
            file: val.file,
            onSuccess: (completeData) => {<!-- -->
                setUploadVal(val => {<!-- -->
                console.log(`The upload is complete, the file is ${<!-- -->completeData?.Location}`);
            },
            onError: (err) => {<!-- -->
                message.error(`File upload failed, please try again later!,${<!-- -->err}`)
            },
        });
}

parameter

Node obtains the temporary key interface: https://github.com/tencentyun/qcloud-cos-sts-sdk/blob/master/nodejs/demo/sts-server.js
The front end uses a temporary key: https://cloud.tencent.com/document/product/436/11459
cos document: https://cloud.tencent.com/document/product/436/14048