Google Apps Script调用亚马逊SP-API获取订单时签名及授权异常排查
Google Apps Script 调用亚马逊SP-API订单接口问题排查过程
开发前置情况
- 目标是基于官方开发者指南要求,开发Google Apps Script脚本,调用
GET /orders/v0/orders接口拉取订单数据 - 目前已完成access_token的生成逻辑,按照接口文档说明,
MarketplaceIds是该接口唯一必填参数 - 因SP-API强制要求AWS V4签名校验,先编写了初版脚本发起调用
初版参考脚本
function GetOrders(){ var access_token = AccessToken(); //Time variables var currentDate = new Date(); var isoDate = currentDate.toISOString(); var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd'); //API variables var end_point = 'https://sellingpartnerapi-eu.amazon.com'; //Credential variables var aws_region = "eu-west-1"; var service = "execute-api"; var termination_string = "aws4_request"; //CanonicalRequest = httpRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload)); //CanonicalRequest components: var httpRequestMethod = 'GET'; var canonicalURI = '/orders/v0/orders'; var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9'; var canonicalheaders = 'host:' + canonicalURI + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate; var signedheaders = 'host;user-agent;x-amz-access-token;x-amz-date'; var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, ""); requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join(""); //Building the canonical request var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + canonicalQueryString + '\n' + canonicalheaders + '\n' + signedheaders + '\n' + requestPayloadHashed; var canonical_signature = Utilities.computeHmacSha256Signature(canonical_string, ACCESS_KEY); var canonical_request = canonical_string + '\n' + canonical_signature; canonical_request = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, canonical_request); //CredentialScope = Date + AWS region + Service + Termination string; //StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest; var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string; var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoDate + '\n' + credential_scope + '\n' + canonical_request; var kSecret = ACCESS_KEY; var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret); var kRegion = Utilities.computeHmacSha256Signature(Utilities.newBlob(aws_region).getBytes(), kDate); var kService = Utilities.computeHmacSha256Signature(Utilities.newBlob(service).getBytes(), kRegion); var kSigning = Utilities.computeHmacSha256Signature(Utilities.newBlob(termination_string).getBytes(), kService); kSigning = kSigning.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join(""); Logger.log('kSigning: ' + kSigning); var signature = Utilities.computeHmacSha256Signature(kSigning, string_to_sign); signature = signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join(""); var options = { 'method': 'GET', 'payload': { 'end_point': end_point, 'path': canonicalURI, 'query_string': canonicalQueryString }, 'headers': { 'x-amz-access-token': access_token, 'x-amz-date': isoDate, 'user-agent': 'GAS Script 1.0 (Javascript)', 'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature, }, } var getOrders = UrlFetchApp.fetch(end_point, options); Logger.log(getOrders); }
第一次报错:InvalidSignature
运行初版脚本时返回403错误,服务端返回InvalidSignature提示,即客户端计算的签名和服务端校验的签名不匹配,同时服务端给出了它端生成的规范请求串、待签名串作为对照。
关于空请求载荷的处理存在疑问:按照AWS签名规则,规范请求串的最后一部分是请求载荷的SHA256哈希值,空载荷需要用空字符串作为哈希输入,当前实现的空GET请求哈希计算逻辑如下,不确定是否遗漏关键步骤:
var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, ""); requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
第一次调整后仍报签名不匹配
对照服务端给出的规范串调整签名计算逻辑后,依然返回InvalidSignature错误。
这期间用亚马逊官方SP-API测试工具验证了手中的Access Key ID和Secret Key有效性,发现工具要求传入订单接口文档中未提及的SellerId参数,暂时不清楚该参数的作用与使用场景。
第二次调整后报错:Unauthorized
按照服务端给出的规范要求,对齐了时间格式、规范请求串拼接规则、签名派生逻辑后,调整后的脚本如下:
function GetOrders(){ var access_token = AccessToken(); //Time variables var currentDate = new Date(); var isoDate = currentDate.toISOString(); var isoString = isoDate.replace(/-/g, "").replace(/:/g, "").replace(/(\.\d{3})/, ""); var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd'); Logger.log('isoDate: ' + isoDate) //API variables var end_point = 'https://sellingpartnerapi-eu.amazon.com'; //Credential variables var aws_region = "eu-west-1"; var service = "execute-api"; var termination_string = "aws4_request"; //CanonicalRequest = httpRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload)); //CanonicalRequest components: var httpRequestMethod = 'GET'; var canonicalURI = '/orders/v0/orders'; var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9'; var canonicalheaders = 'host:' + "sellingpartnerapi-eu.amazon.com" + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate; var signedheaders = 'host;x-amz-access-token;x-amz-date'; var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, ""); requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join(""); //Building the canonical request var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + "marketplaceId=A1PA6795UKMFR9" + '\n' + canonicalheaders + '\n\n' + signedheaders + '\n' + requestPayloadHashed; Logger.log('canonical_string: ' + canonical_string) var canonical_signature = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, canonical_string); canonical_request = canonical_signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join(""); Logger.log("canonical_request: " + canonical_request) //CredentialScope = Date + AWS region + Service + Termination string; //StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest; var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string; var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoString + '\n' + credential_scope + '\n' + canonical_request; Logger.log("string_to_sign: " + string_to_sign); var kSecret = ACCESS_KEY; var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret); var kRegion = Utilities.computeHmacSha256Signature(toBytes(aws_region), kDate); var kService = Utilities.computeHmacSha256Signature(toBytes(service), kRegion); var kSigning = Utilities.computeHmacSha256Signature(toBytes(termination_string), kService); Logger.log('kSigning: ' + kSigning); var signature = hex(Utilities.computeHmacSha256Signature(toBytes(string_to_sign), kSigning)); Logger.log('signature: ' + signature) var options = { 'method': 'GET', 'headers': { 'x-amz-access-token': access_token, 'x-amz-date': isoDate, 'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature, }, 'muteHttpExceptions': true } var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options); Logger.log(getOrders); }
调整后脚本不再触发签名校验错误,新的服务端响应为未授权访问:
{ "errors": [ { "message": "Access to requested resource is denied.", "code": "Unauthorized", "details": "" } ] }
当前问题初步定位
根据官方应用注册规则,注册应用时填写的IAM ARN必须是绑定了对应权限策略的IAM角色,不能填写IAM用户的ARN,否则SP-API请求会直接被拒绝。目前初步判断这个未授权错误是注册应用时误填了IAM用户的ARN导致的,准备修正该配置后再次测试。
内容的提问来源于stack exchange,提问作者Luis Alberto Delgado de la Flo
相关产品推荐
相关产品推荐

