使用AWS Signature Version 4签名POST请求遇403,如何正确实现?
解决AWS SigV4 POST请求签名的哈希与编码问题
核心问题定位
你碰到的403错误,核心原因几乎肯定是请求体哈希计算不符合规范,或者签名流程中的字符编码/格式处理和官方要求不一致——Postman会严格遵循SigV4标准处理,而手动转写JS代码时,容易在编码细节上出错。
正确的SigV4 POST签名关键步骤及代码实现
1. 计算请求体的SHA-256哈希
这是最容易出错的环节,必须严格遵守:
- 空请求体必须使用固定哈希值:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 - 非空请求体必须基于原始二进制数据计算哈希,不能先转成字符串再处理;浏览器环境要注意
Blob/FormData的读取方式 - 计算结果必须转成小写十六进制字符串
Node.js 实现:
const crypto = require('crypto'); function calculatePayloadHash(payload) { if (!payload || payload.length === 0) { return 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; } return crypto.createHash('sha256').update(payload).digest('hex').toLowerCase(); }
浏览器环境(使用Web Crypto API):
async function calculatePayloadHash(payload) { if (!payload) { return 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; } const encoder = new TextEncoder(); const data = encoder.encode(payload); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase(); }
2. 构建规范请求字符串(Canonical Request)
必须严格按照SigV4规则拼接,注意细节:
- 每行末尾必须是LF换行符(
\n),不能用CRLF(\r\n) - 请求头需按字母排序后拼接,格式必须是
key:value(不能有多余空格) - 必须包含
host和x-amz-date头;POST请求若有请求体,需包含content-type头 - 最后一行是请求体的哈希值
示例代码:
function buildCanonicalRequest(method, url, headers, payloadHash) { const parsedUrl = new URL(url); const canonicalUri = encodeURIComponent(parsedUrl.pathname).replace(/%2F/g, '/'); const canonicalQuerystring = parsedUrl.search.slice(1); // 排序并格式化请求头 const sortedHeaders = Object.keys(headers).sort().map(key => `${key.toLowerCase()}:${headers[key].trim()}`).join('\n'); const signedHeaders = Object.keys(headers).sort().map(key => key.toLowerCase()).join(';'); return [ method, canonicalUri, canonicalQuerystring, sortedHeaders, '', // 空行分隔请求头与哈希值 signedHeaders, payloadHash ].join('\n'); }
3. 构建待签名字符串(String to Sign)
注意格式要求:
x-amz-date必须是YYYYMMDD'T'HHMMSS'Z'格式(比如20240520T123456Z)- 凭证范围格式为
YYYYMMDD/region/service/aws4_request
示例代码:
function buildStringToSign(canonicalRequest, amzDate, credentialScope) { const canonicalRequestHash = crypto.createHash('sha256').update(canonicalRequest).digest('hex').toLowerCase(); return [ 'AWS4-HMAC-SHA256', amzDate, credentialScope, canonicalRequestHash ].join('\n'); }
4. 计算签名密钥与最终签名
需通过HMAC-SHA256逐步推导签名密钥:
function getSignatureKey(secretKey, dateStamp, regionName, serviceName) { const kDate = crypto.createHmac('sha256', `AWS4${secretKey}`).update(dateStamp).digest(); const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest(); const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest(); const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest(); return kSigning; } function calculateSignature(stringToSign, signatureKey) { return crypto.createHmac('sha256', signatureKey).update(stringToSign).digest('hex').toLowerCase(); }
浏览器环境额外注意事项
- 绝对不能在浏览器中暴露AWS永久密钥,建议通过后端代理完成签名,或使用AWS Cognito临时凭证
- 确保请求体的序列化格式和Postman完全一致(比如JSON要严格序列化,不能有多余空格)
- 检查
x-amz-date和dateStamp的格式:dateStamp是纯日期YYYYMMDD,不含时间部分
验证方法
把你的代码生成的内容和Postman对比:
- 在Postman中开启AWS Signature授权,发送请求后查看
Authorization头,拆分出凭证范围、签名头、签名值 - 对比代码生成的规范请求字符串和Postman内部生成的版本(可通过Postman的“代码生成”功能导出curl,反向推导规范请求)
- 重点核对请求体哈希值、换行符格式、请求头排序这几个环节
内容的提问来源于stack exchange,提问作者JeremiahDuane
相关产品推荐
相关产品推荐

