2016-10-17 290 views
3

我需要为亚马逊MWS生成签名,并决定仅使用Delphi附带的组件和类来查找解决方案。因为我使用Indy作为自己的HTTP帖子,所以使用Indy类来计算RFC 2104兼容的HMAC似乎是个好主意。使用Delphi XE7和Indy类创建亚马逊MWS签名

对于其他人,谁在亚马逊整合工作,“规范化查询字符串”的创作在亚马逊教程是很好的解释:http://docs.developer.amazonservices.com/en_DE/dev_guide/DG_ClientLibraries.html 小心,只使用断行#10,如#13#10或#13将失败,并签名错误。如问题#23573799所述,根据TIdHttp版本添加“:443”到亚马逊端点(主机)也很重要。

要创建一个有效的签名,我们必须使用SHA256计算一个HMAC,其中包含查询字符串和我们从注册后从亚马逊获得的SecretKey,然后结果必须在BASE64中编码。

正确生成查询字符串,并且与亚马逊Scratchpad创建的字符串相同。但由于签名不正确,通话失败。

经过一些测试后,我意识到从我的查询字符串中得到的签名与我在使用PHP生成时的结果不一样。 PHP的结果被认为是正确的,因为我的PHP解决方案很长一段时间与亚马逊一起工作,德尔菲的结果是不同的,这是不正确的。

为了使测试更容易,我使用'1234567890'作为查询字符串的值,'ABCDEFG'作为SecretKey的替换值。当我使用Delphi获得的结果与使用PHP获得的结果相同时,我相信问题应该得到解决。

这是我如何得到正确的结果与PHP:

echo base64_encode(hash_hmac('sha256', '1234567890', 'ABCDEFG', TRUE)); 

这显示

aRGlc3RY1pKmKX0hvorkVKNcPigiJX2rksqXzlAeCLg= 

以下德尔福XE7代码返回错误结果的结果,在使用印版本附带德尔福XE7:

uses 
  IdHash, IdHashSHA, IdHMACSHA1, IdSSLOpenSSL, IdGlobal, IdCoderMIME; 

function GenerateSignature(const AData, AKey: string): string; 
var 
   AHMAC: TIdBytes; 
begin 
     IdSSLOpenSSL.LoadOpenSSLLibrary; 

     With TIdHMACSHA256.Create do 
      try 
         Key:= ToBytes(AKey, IndyTextEncoding_UTF16LE); 
         AHMAC:= HashValue(ToBytes(AData, IndyTextEncoding_UTF16LE)); 
         Result:= TIdEncoderMIME.EncodeBytes(AHMAC); 
      finally 
         Free; 
      end; 
end; 

这里的结果显示在Memo

Memo.Lines.Text:= GenerateSignature('1234567890', 'ABCDEFG'); 

是:

jg6Oddxvv57fFdcCPXrqGWB9YD5rSvtmGnZWL0X+y0Y= 

我相信这个问题有什么做的编码,所以我做了周围的一些研究。正如亚马逊教程(链接参见上文)所述,亚马逊期望采用utf8编码。

由于Indy函数“ToBytes”期望一个字符串,它是我的Delphi版本中的一个UnicodeString,所以我放弃使用其他字符串类型作为参数或变量的UTF8String进行测试,但我不知道utf8应该在何处到位。此外,我不知道我在上面的代码中使用的编码是否正确。 我选择UTF16LE是因为UnicodeString是utf16编码(详见http://docwiki.embarcadero.com/RADStudio/Seattle/en/String_Types_(Delphi)),LE(Little-Endian)是现代机器上最常用的。德尔福的TEncodings本身也有“Unicode”和“BigEndianUnicode”,所以“Unicode”似乎是LE和某种“标准”的Unicode。 当然,我在上面的代码中测试过使用IndyTextEncoding_UTF8而不是IndyTextEncoding_UTF16LE,但它无论如何都不起作用。

由于

TIdEncoderMIME.EncodeBytes(AHMAC); 

首先写入TidBytes到Stream,然后用8位编码读取这一切,这可能是问题也是一个来源,所以我还与

Result:= BytesToString(AHMAC, IndyTextEncoding_UTF16LE); 
Result:= TIdEncoderMIME.EncodeString(Result, IndyTextEncoding_UTF16LE); 

测试但结果是一样的。

如果你喜欢看主代码创建的要求,这就是:

function TgboAmazon.MwsRequest(const AFolder, AVersion: string; 
    const AParams: TStringList; const AEndPoint: string): string; 
var 
    i: Integer; 
    SL: TStringList; 
    AMethod, AHost, AURI, ARequest, AStrToSign, APath, ASignature: string; 
    AKey, AValue, AQuery: string; 
    AHTTP: TIdHTTP; 
    AStream, AResultStream: TStringStream; 
begin 
    AMethod:= 'POST'; 
    AHost:= AEndPoint; 
    AURI:= '/' + AFolder + '/' + AVersion; 

    AQuery:= ''; 
    SL:= TStringList.Create; 
    try 
     SL.Assign(AParams); 
     SL.Values['AWSAccessKeyId']:= FAWSAccessKeyId; 
     SL.Values['SellerId']:= FSellerId; 
     FOR i:=0 TO FMarketplaceIds.Count-1 DO 
     begin 
       SL.Values['MarketplaceId.Id.' + IntToStr(i+1)]:= FMarketplaceIds[i]; 
     end; 

     SL.Values['Timestamp']:= GenerateTimeStamp(Now); 
     SL.Values['SignatureMethod']:= 'HmacSHA256'; 
     SL.Values['SignatureVersion']:= '2'; 
     SL.Values['Version']:= AVersion; 

     FOR i:=0 TO SL.Count-1 DO 
     begin 
       AKey:= UrlEncode(SL.Names[i]); 
       AValue:= UrlEncode(SL.ValueFromIndex[i]); 
       SL[i]:= AKey + '=' + AValue; 
     end; 

     SortList(SL); 
     SL.Delimiter:= '&'; 
     AQuery:= SL.DelimitedText; 

     AStrToSign:= AMethod + #10 + AHost + #10 + AURI + #10 + AQuery; 
     TgboUtil.ShowMessage(AStrToSign); 

     ASignature:= GenerateSignature(AStrToSign, FAWSSecretKey); 
     TgboUtil.ShowMessage(ASignature); 

     APath:= 'https://' + AHost + AURI + '?' + AQuery + '&Signature=' + Urlencode(ASignature); 
     TgboUtil.ShowMessage(APath); 
    finally 
     SL.Free; 
    end; 

    AHTTP:= TIdHTTP.Create(nil); 
    try 
     AHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(AHTTP); 
     AHTTP.Request.ContentType:= 'text/xml'; 
     AHTTP.Request.Connection:= 'Close'; 
     AHTTP.Request.CustomHeaders.Add('x-amazon-user-agent: MyApp/1.0 (Language=Delphi/XE7)'); 
     AHTTP.HTTPOptions:= AHTTP.HTTPOptions + [hoKeepOrigProtocol]; 
     AHTTP.ProtocolVersion:= pv1_0; 
     AStream:= TStringStream.Create; 
     AResultStream:= TStringStream.Create; 
     try 
      AHTTP.Post(APath, AStream, AResultStream); 
      Result:= AResultStream.DataString; 
      ShowMessage(Result); 
     finally 
      AStream.Free; 
      AResultStream.Free; 
     end; 
    finally 
     AHTTP.Free; 
    end; 
end; 

的URLEncode和GenerateTimestamp是我自己的职能和他们做了什么名字的承诺,sortlist中是我自己的过程,它排序字符串列表按照亚马逊的要求以字节顺序排列,TgboUtil.ShowMessage是我自己的ShowMessage替代方法,它显示包含所有字符的完整消息并仅用于调试。 http协议仅用于测试,因为我得到了403(拒绝的权限),因为HTTP返回的时间较早。我只是想排除这个问题,因为indy文档说,协议版本1.1被认为是不完整的,因为有问题的服务器答案。

这里有几个关于amazon mws主题的帖子,但是这个具体问题似乎是新的。

这里的这个问题可能会帮助那些目前还没有到达的人,也希望有人可以提供一个解决方案,只需在Delphi中获得与PHP相同的签名值。

预先感谢您。

+0

请参阅雷米勒博的回答一些好的解释了我的代码更改,所以它会工作,结束。因此,感兴趣的访问者可以更好地理解问题及其解决方案,我决定保留未经编辑的代码,除了'TIdSSLIOHandlerSocketOpenSSL' – KaiW

回答

3

使用Indy 10的最新SVN快照,我无法重现您的签名问题。在使用UTF-8时,您的示例键+值数据在Delphi中与PHP输出中产生相同的结果。所以,你的GenerateSignature()功能是好的,但前提是:

  1. 您使用IndyTextEncoding_UTF8代替IndyTextEncoding_UTF16LE

  2. 确保ADataAKey包含有效的输入数据。

此外,您应该确保TIdHashSHA256.IsAvailable返回true,否则TIdHashHMACSHA256.HashValue()将失败。例如,如果OpenSSL加载失败,可能会发生这种情况。

试试这个:

function GenerateSignature(const AData, AKey: string): string; 
var 
    AHMAC: TIdBytes; 
begin 
    IdSSLOpenSSL.LoadOpenSSLLibrary; 

    if not TIdHashSHA256.IsAvailable then 
    raise Exception.Create('SHA-256 hashing is not available!'); 

    with TIdHMACSHA256.Create do 
    try 
    Key := IndyTextEncoding_UTF8.GetBytes(AKey); 
    AHMAC := HashValue(IndyTextEncoding_UTF8.GetBytes(AData)); 
    finally 
    Free; 
    end; 

    Result := TIdEncoderMIME.EncodeBytes(AHMAC); 
end; 

话虽这么说,有相当多的问题,您MwsRequest()功能:

  1. 你泄漏TIdSSLIOHandlerSocketOpenSSL对象。您没有为其分配Owner,并且TIdHTTP在分配给其IOHandler属性时未取得所有权。实际上,在您的示例中,分配IOHanlder实际上是可选的,请参阅New HTTPS functionality for TIdHTTP

  2. 您正在将AHTTP.Request.ContentType设置为错误的媒体类型。您没有发送XML数据,因此不要将媒体类型设置为'text/xml'。在这种情况下,您需要将其设置为'application/x-www-form-urlencoded'

  3. 当呼叫AHTTP.Post()时,您的AStream流为空,所以您实际上并未向服务器发送任何数据。您正在将AQuery数据放入URL本身的查询字符串中,但它实际上属于AStream。如果要发送URL查询字符串中的数据,则必须使用TIdHTTP.Get()而不是TIdHTTP.Post(),并将AMethod的值更改为'GET'而不是'POST'

  4. 您正在使用的TIdHTTP.Post()版本填充输出TStream。您正在使用TStringStream将响应转换为String,而不考虑响应数据使用的实际字符集。由于您没有在构造函数TStringStream中指定任何TEncoding对象,因此它将使用​​进行解码,这可能不会(也可能不会)匹配响应的实际字符集。您应该使用返回StringPost()的其他版本,以便TIdHTTP可以根据HTTPS服务器报告的实际字符集解码响应数据。

尝试一些更喜欢这个:

function TgboAmazon.MwsRequest(const AFolder, AVersion: string; 
    const AParams: TStringList; const AEndPoint: string): string; 
var 
    i: Integer; 
    SL: TStringList; 
    AMethod, AHost, AURI, AQuery, AStrToSign, APath, ASignature: string; 
    AHTTP: TIdHTTP; 
begin 
    AMethod := 'POST'; 
    AHost := AEndPoint; 
    AURI := '/' + AFolder + '/' + AVersion; 

    AQuery := ''; 
    SL := TStringList.Create; 
    try 
    SL.Assign(AParams); 

    SL.Values['AWSAccessKeyId'] := FAWSAccessKeyId; 
    SL.Values['SellerId'] := FSellerId; 
    for i := 0 to FMarketplaceIds.Count-1 do 
    begin 
     SL.Values['MarketplaceId.Id.' + IntToStr(i+1)] := FMarketplaceIds[i]; 
    end; 

    SL.Values['Timestamp'] := GenerateTimeStamp(Now); 
    SL.Values['SignatureMethod'] := 'HmacSHA256'; 
    SL.Values['SignatureVersion'] := '2'; 
    SL.Values['Version'] := AVersion; 
    SL.Values['Signature'] := ''; 

    SortList(SL); 

    for i := 0 to SL.Count-1 do 
     SL[i] := UrlEncode(SL.Names[i]) + '=' + UrlEncode(SL.ValueFromIndex[i]); 

    SL.Delimiter := '&'; 
    SL.QuoteChar := #0; 
    SL.StrictDelimiter := True; 
    AQuery := SL.DelimitedText; 
    finally 
    SL.Free; 
    end; 

    AStrToSign := AMethod + #10 + Lowercase(AHost) + #10 + AURI + #10 + AQuery; 
    TgboUtil.ShowMessage(AStrToSign); 

    ASignature := GenerateSignature(AStrToSign, FAWSSecretKey); 
    TgboUtil.ShowMessage(ASignature); 

    APath := 'https://' + AHost + AURI; 
    TgboUtil.ShowMessage(APath); 

    AHTTP := TIdHTTP.Create(nil); 
    try 
    // this is actually optional in this example... 
    AHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(AHTTP); 

    AHTTP.Request.ContentType := 'application/x-www-form-urlencoded'; 
    AHTTP.Request.Connection := 'close'; 
    AHTTP.Request.UserAgent := 'MyApp/1.0 (Language=Delphi/XE7)'; 
    AHTTP.Request.CustomHeaders.Values['x-amazon-user-agent'] := 'MyApp/1.0 (Language=Delphi/XE7)'; 
    AHTTP.HTTPOptions := AHTTP.HTTPOptions + [hoKeepOrigProtocol]; 
    AHTTP.ProtocolVersion := pv1_0; 

    AStream := TStringStream.Create(AQuery + '&Signature=' + Urlencode(ASignature); 
    try 
     Result := AHTTP.Post(APath, AStream); 
     ShowMessage(Result); 
    finally 
     AStream.Free; 
    end; 
    finally 
    AHTTP.Free; 
    end; 
end; 

然而,由于响应记录为XML,这将是更好的回报为TStream(不使用TStringStream给调用者的响应,虽然)或TBytes而不是String。这样,代替Indy对字节进行解码,让XML解析器自己解码原始的字节。 XML有来自HTTP分开自己的字符集的规则,所以让XML解析器来完成它的工作对您:

procedure TgboAmazon.MwsRequest(...; Response: TStream); 
var 
    ... 
begin 
    ... 
    AHTTP.Post(APath, AStream, Response); 
    ... 
end; 
+0

的泄漏,谢谢你的详细解答。我尝试过使用不同字符集来解决这个问题的很多变体,但似乎我没有在最后使用最近的解决方案。现在用UTF8'GenerateSignature'对我来说工作得很好。我也接受你的修改,并按照你的建议更改我的代码。我用'text/xml'作为内容类型,因为亚马逊明确要求。但它也适用于'application/x-www-form-urlencoded',所以它似乎并不在意,所以我按照建议使用它。 – KaiW