需求:
页面和后台使用aksk进行签名校验,普通JSON参数签名没问题,但使用formData上传文件时签名总是无法通过后台校验
关键点:
1、浏览器在传递formData格式数据时会自动随机boundary,这样页面无法在请求发起前拿到随机boundary,造成前后台计算入参不一致
2、formData格式的数据是否可以直接用来计算其hash
解决方案:
1、针对随机boundary问题,通过手动指定解决
2、因为随机boundary问题,暂未找到直接对formData格式数据签名方式,构造其结构转二进制实现
关键代码:
1、将formData内容拆成两部分计算计算其二进制数据
js
const fields = {
fileName: fileInfo.fileName,
chunk: currentChunk + 1,
chunks: totalChunks,
uploadId: fileInfo.uploadId,
fileType: fileType.value
}
const files = {
file: chunk
}
const boundary = '----MyCustomBoundaryABC'
2、拼接二进制数据
js
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 = 0
for (const chunk of chunks) {
body.set(chunk, offset)
offset += chunk.length
}
return body
}
3、使用二进制数据进行签名
js
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、签名实现
ts
import { SHA256, HmacSHA256, enc } from 'crypto-js';
function useAuth() {
function hmacWithSHA256(message, secretKey) {
// 计算 HMAC-SHA256
const 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) + 10000
const 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 }
