簡體   English   中英

使用Delphi XE7和Indy類創建amazon MWS簽名

[英]Creating an amazon MWS signature with Delphi XE7 and Indy classes

我需要為amazon MWS生成一個簽名,並決定找到一個只包含Delphi附帶的組件和類的解決方案。 因為我使用Indy作為HTTP帖子本身,所以使用Indy類來計算符合RFC 2104的HMAC似乎是個好主意。

對於其他從事亞馬遜集成工作的人來說,亞馬遜教程中很好地解釋了“規范化查詢字符串”的創建: http//docs.developer.amazonservices.com/en_DE/dev_guide/DG_ClientLibraries.html小心,只是使用#10進行換行,因為#13#10或#13將失敗,簽名錯誤。 根據TIdHttp版本,將“:443”添加到亞馬遜端點(主機)也很重要,如問題#23573799中所述。

要創建一個有效的簽名,我們必須計算一個帶有SHA256的HMAC,其中包含查詢字符串和我們在注冊后從亞馬遜獲得的SecretKey,然后,結果必須在BASE64中編碼。

查詢字符串已正確生成,並與amazon Scratchpad創建的字符串相同。 但是呼叫失敗了,因為簽名不正確。

經過一些測試,我意識到我從查詢字符串中獲得的簽名與我使用PHP生成它時得到的結果不一樣。 PHP結果被認為是正確的,因為我的PHP解決方案只是與亞馬遜一起使用很長一段時間,德爾福結果是不同的,這是不正確的。

為了使測試更容易,我使用'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中獲得與PHP相同的簽名值。

先感謝您。

使用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實際上是可選的,請參閱TIdHTTP的新HTTPS功能

  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進行解碼,這可能不會(也可能不會)匹配響應的實際字符集。 您應該使用返回String的其他版本的Post() ,以便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返回給調用者。 這樣,讓您的XML解析器自己解碼原始字節,而不是Indy解碼字節。 XML有自己的與HTTP分離的字符集規則,因此讓XML解析器為您完成工作:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM