需求:
頁面和后臺使用aksk進行簽名校驗,普通JSON參數簽名沒問題,但使用formData上傳文件時簽名總是無法通過后臺校驗
關鍵點:
1、瀏覽器在傳遞formData格式數據時會自動隨機boundary,這樣頁面無法在請求發起前拿到隨機boundary,造成前后臺計算入參不一致
2、formData格式的數據是否可以直接用來計算其hash
解決方案:
1、針對隨機boundary問題,通過手動指定解決
2、因為隨機boundary問題,暫未找到直接對formData格式數據簽名方式,構造其結構轉二進制實現
關鍵代碼:
1、將formData內容拆成兩部分計算計算其二進制數據
const fields = {fileName: fileInfo.fileName,chunk: currentChunk + 1,chunks: totalChunks,uploadId: fileInfo.uploadId,fileType: fileType.value}const files = {file: chunk}const boundary = '----MyCustomBoundaryABC'
2、拼接二進制數據
async function buildMultipartFormData(fields, files, boundary) {const CRLF = '\r\n'const encoder = new TextEncoder()const chunks = []const pushText = (text) => chunks.push(encoder.encode(text))// 普通字段for (const [name, value] of Object.entries(fields)) {pushText(`--${boundary}${CRLF}`)pushText(`Content-Disposition: form-data; name="${name}"${CRLF}${CRLF}`)pushText(`${value}${CRLF}`)}// 文件字段for (const [name, file] of Object.entries(files)) {const filename = file.name || 'blob'const mimeType = file.type || 'application/octet-stream'pushText(`--${boundary}${CRLF}`)pushText(`Content-Disposition: form-data; name="${name}"; filename="${filename}"${CRLF}`)pushText(`Content-Type: ${mimeType}${CRLF}${CRLF}`)const fileBuffer = new Uint8Array(await file.arrayBuffer())chunks.push(fileBuffer)pushText(CRLF)}// 結尾pushText(`--${boundary}--${CRLF}`)// 合并所有 Uint8Array 塊const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)const body = new Uint8Array(totalLength)let offset = 0for (const chunk of chunks) {body.set(chunk, offset)offset += chunk.length}return body
}
3、使用二進制數據進行簽名
buildMultipartFormData(fields, files, boundary).then(async (bodyBinary) => {// 查看構造的內容(可選)const auth = await createAuth(bodyBinary)// 發送請求fetch('/cos/upload', {method: 'POST',headers: {'Content-Type': `multipart/form-data; boundary=${boundary}`,'Authorization': auth},body: bodyBinary})})
4、簽名實現
import { SHA256, HmacSHA256, enc } from 'crypto-js';
function useAuth() {function hmacWithSHA256(message, secretKey) {// 計算 HMAC-SHA256const hmac = HmacSHA256(message, secretKey);// 返回十六進制字符串(小寫)return hmac.toString(enc.Hex);}function generateRandomString(length = 16) {const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';let result = '';for (let i = 0; i < length; i++) {result += chars.charAt(Math.floor(Math.random() * chars.length));}return result;}async function hash256(data) {const str = typeof data === 'string' ? data : JSON.stringify(data);return SHA256(str).toString();}async function createAuth(bodyRaw) {const appid = 'app_demo'const access = 'ak_demo'const sk = 'sk_demo'const expiretime = Math.floor(Date.now() / 1000) + 10000const nonce = generateRandomString()let hashBody;if (bodyRaw instanceof Uint8Array) {hashBody = await calculateBinaryHash(bodyRaw)} else {hashBody = await hash256(typeof bodyRaw === 'string' ? bodyRaw : JSON.stringify(bodyRaw))}const signature = hmacWithSHA256(hashBody + expiretime + nonce, sk)const res = `appid=${appid},access=${access},signature=${signature},nonce=${nonce},expiretime=${expiretime}`return res}const calculateBinaryHash = async (binaryData: Uint8Array | ArrayBuffer): Promise<string> => {// 瀏覽器環境if (typeof window !== 'undefined' && crypto.subtle) {const hashBuffer = await crypto.subtle.digest('SHA-256', binaryData);const hashArray = Array.from(new Uint8Array(hashBuffer));return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');}};return {createAuth}
}export { useAuth }