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

使用Delphi XE7和Indy类创建amazon MWS签名,delphi,encryption,unicode,indy10,amazon-mws,Delphi,Encryption,Unicode,Indy10,Amazon Mws,我需要为AmazonMWS生成一个签名,并决定找到一个只包含Delphi附带的组件和类的解决方案。因为我将Indy用于HTTP post本身,所以使用Indy类来计算符合RFC2104的HMAC似乎是个好主意 对于从事amazon集成工作的其他人来说,“规范化查询字符串”的创建在amazon教程中有很好的解释: 小心,只需使用#10进行换行,因为#13#10或#13将因签名错误而失败。在amazon端点(主机)中添加“:443”也很重要,具体取决于TIDWTTP版本,如问题23573799所述

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

对于从事amazon集成工作的其他人来说,“规范化查询字符串”的创建在amazon教程中有很好的解释: 小心,只需使用#10进行换行,因为#13#10或#13将因签名错误而失败。在amazon端点(主机)中添加“:443”也很重要,具体取决于TIDWTTP版本,如问题23573799所述

为了创建一个有效的签名,我们必须使用查询字符串和注册后从amazon获得的SecretKey计算一个带有SHA256的HMAC,然后,结果必须用BASE64编码

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

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

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

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

echo base64_encode(hash_hmac('sha256', '1234567890', 'ABCDEFG', TRUE));
这表明了

aRGlc3RY1pKmKX0hvorkVKNcPigiJX2rksqXzlAeCLg=
使用Delphi XE7附带的indy版本时,以下Delphi 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.Lines.Text:= GenerateSignature('1234567890', 'ABCDEFG'); 
是:

我相信这个问题与编码有关,所以我做了一些研究。正如amazon教程(链接见上文)所述,amazon希望使用utf8编码

由于Indy函数“ToBytes”需要一个字符串,这在我的Delphi版本中是一个UnicodeString,因此我退出了使用其他字符串类型作为参数或变量的UTF8String进行测试,但我只是不知道utf8应该放在哪里。我也不知道我在上面的代码中使用的编码是否正确。 我选择UTF16LE是因为UnicodeString是utf16编码的(详情请参见),而LE(Little Endian)是现代机器上最常用的。Delphi本身的十位编码还有“Unicode”和“BigEndianUnicode”,所以“Unicode”似乎是LE和某种“标准”Unicode。 当然,我在上面的代码中测试了使用IndyTextEncoding_UTF8而不是IndyTextEncoding_UTF16LE,但它无论如何都不起作用

因为

TIdEncoderMIME.EncodeBytes(AHMAC);
首先将TidBytes写入流,然后使用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的要求按字节顺序对stringlist进行排序,TgboUtil.ShowMessage是我自己的ShowMessage替代方案,它显示了包含所有字符的完整消息,并且仅用于调试。http协议是1.0,仅用于测试,因为我在前面得到了403(权限被拒绝)作为http返回。我只是想排除这个问题,正如indy文档所说,协议版本1.1被认为是不完整的,因为有问题的服务器答案

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

这里的这个问题可能会帮助一些还没到这一步的人,但我也希望有人能提供一个解决方案,在Delphi中获得与PHP相同的签名值


提前感谢。

使用Indy 10的最新SVN快照,我无法重现您的签名问题。当使用UTF-8时,示例键+值数据在Delphi中生成与PHP输出相同的结果。因此,只要满足以下条件,您的
GenerateSignature()
函数就可以了:

  • 您使用
    IndyTextEncoding\u UTF8
    而不是
    IndyTextEncoding\u UTF16LE

  • 确保
    AData
    AKey
    包含有效的输入数据

  • 另外,您应该确保
    tidhasha256.IsAvailable
    返回true,否则
    tidhashmacsha256.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; 
    
    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;
    
    也就是说,您的
    MwsRequest()
    函数有很多问题:

  • 您正在泄漏
    TIdSSLIOHandlerSocketOpenSSL
    对象。您没有将
    所有者
    分配给它,并且
    TIdHTTP
    在分配给其
    IOHandler
    属性时不拥有所有权。事实上,在您的示例中,分配
    IOHanlder
    实际上是可选的,请参阅了解原因

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

  • 调用
    AHTTP.Post()
    时,您的
    AStream
    流为空,因此您实际上没有向服务器发布任何数据。您正在将
    AQuery
    数据放入URL本身的查询字符串中,但它实际上属于
    AStream
    。如果要发送URL查询字符串中的数据,必须使用
    TIdHTTP.Get()
    而不是
    TIdHTTP.Post()
    ,并更改
    AMethod
    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;
    
    procedure TgboAmazon.MwsRequest(...; Response: TStream);
    var
      ...
    begin
      ...
      AHTTP.Post(APath, AStream, Response);
      ...
    end;