使用Delphi XE7和Indy类创建amazon MWS签名

时间:2016-10-17 14:03:16

标签: delphi encryption unicode indy10 amazon-mws

我需要为amazon MWS生成一个签名,并决定找到一个只包含Delphi附带的组件和类的解决方案。因为我使用Indy作为HTTP帖子本身,所以使用Indy类来计算符合RFC 2104的HMAC似乎是个好主意。

对于其他从事亚马逊整合的人,创建了" Canonicalized Query String"在亚马逊教程中很好地解释了:http://docs.developer.amazonservices.com/en_DE/dev_guide/DG_ClientLibraries.html 要小心,只需使用#10进行换行,因为#13#10或#13会因错误的签名而失败。添加":443"也可能很重要。到amazon端点(主机),取决于TIdHttp版本,如问题#23573799中所述。

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

正确生成查询字符串,并且与amazon Scratchpad创建的字符串相同。但是呼叫失败了,因为签名不正确。

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

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

以下是我如何使用PHP获得正确的结果:

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

这显示了

的结果
aRGlc3RY1pKmKX0hvorkVKNcPigiJX2rksqXzlAeCLg=

以下Delphi XE7代码返回错误的结果,同时使用Delphi XE7附带的indy版本:

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.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)最常用于现代机器上。 Delphi的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是我自己的程序,按照amazon的请求按字节顺序对字符串列表进行排序,TgboUtil.ShowMessage是我自己的ShowMessage替代方案,显示完整的消息所有字符,仅用于调试。 http协议只有1.0用于测试,因为我之前获得了403(权限被拒绝)作为HTTP返回。我只想将此问题排除在indy文档所说的问题之外,因为服务器答案有问题,协议版本1.1被认为是不完整的。

这里有几篇关于亚马逊mws话题的帖子,但这个具体问题似乎是新的。

这里的这个问题可能会帮助那些到目前为止还没有到来的人,但我也希望有人可以提供一个解决方案来获得与Delphi相同的签名值。

提前谢谢。

1 个答案:

答案 0 :(得分:3)

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

  1. 您使用IndyTextEncoding_UTF8代替IndyTextEncoding_UTF16LE

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

  3. 此外,您应确保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对象。您没有为其分配OwnerTIdHTTP在分配给IOHandler媒体资源时不会获得所有权。实际上,在您的示例中,分配IOHanlder实际上是可选的,请参阅New HTTPS functionality for TIdHTTP了解原因。

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

    3. 调用AHTTP.Post()时,您的AStream信息流为空,因此您实际上并未向服务器发布任何数据。您将AQuery数据放在网址本身的查询字符串中,但它实际上属于AStream。如果您要在网址查询字符串中发送数据,则必须使用TIdHTTP.Get()而不是TIdHTTP.Post(),并将AMethod值更改为'GET'而不是{{1} }}

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

    5. 尝试更像这样的东西:

      Post()

      但是,由于响应被记录为XML,因此最好将响应作为String(不使用TIdHTTP)或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; 返回给调用者作为TStream。这样,让您的XML解析器自己解码 raw 字节,而不是Indy解码字节。 XML有自己的charset规则,它们与HTTP分开,所以让XML解析器为你完成它的工作:

      TStringStream