使用 Delphi XE7 和 Indy 类 创建亚马逊 MWS 签名

Creating an amazon MWS signature with Delphi XE7 and Indy classes

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

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

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

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

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

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

这是我如何使用 PHP 获得正确结果的方法:

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

这显示了

的结果
aRGlc3RY1pKmKX0hvorkVKNcPigiJX2rksqXzlAeCLg=

以下Delphi XE7代码return是错误的结果,而使用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=

我认为问题与编码有关,因此我对此进行了一些研究。正如亚马逊教程(link 见上文)所述,亚马逊需要 utf8 编码。

由于 Indy 函数 "ToBytes" 需要一个字符串,在我的 Delphi 版本中它是一个 UnicodeString,我停止使用其他字符串类型作为参数或变量的 UTF8String 进行测试,但我只是不这样做知道 utf8 应该放在哪里。我也不知道我在上面的代码中使用的编码是否正确。 我选择 UTF16LE,因为 UnicodeString 是 utf16 编码的(有关详细信息,请参阅 http://docwiki.embarcadero.com/RADStudio/Seattle/en/String_Types_(Delphi))并且 LE(小端)在现代机器上最常用。 Delphi 本身的 TEncodings 也有 "Unicode" 和 "BigEndianUnicode",所以 "Unicode" 似乎是 LE 和某种 "standard" 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协议是1.0,只是为了测试,因为我之前得到了一个403(permission denied)作为HTTPreturn。正如 indy 文档所说,我只是想排除这个问题,因为服务器答案有问题,协议版本 1.1 被认为是不完整的。

这里有几个关于亚马逊 mws 主题的 post,但那个具体问题似乎是新问题。

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

提前致谢。

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

  1. 您使用 IndyTextEncoding_UTF8 而不是 IndyTextEncoding_UTF16LE

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

此外,您应该确保 TIdHashSHA256.IsAvailable return 为真,否则 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. 您正在使用填充输出 TStreamTIdHTTP.Post() 版本。您正在使用 TStringStream 将响应转换为 String,而不考虑响应数据使用的实际字符集。由于您没有在 TStringStream 构造函数中指定任何 TEncoding 对象,它将使用 TEncoding.Default 进行解码,这可能不会(也可能不会)匹配响应的实际字符集。您应该使用 Post() 的另一个版本 return 是 String,这样 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,最好将对调用者的响应 return 作为 TStream(不使用 TStringStream , though) 或 TBytes 而不是 String。这样,不是 Indy 解码字节,而是让您的 XML 解析器自行解码 raw 字节。 XML 有自己独立于 HTTP 的字符集规则,所以让 XML 解析器为您完成它的工作:

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