[英]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()
函數很好,前提是:
您使用IndyTextEncoding_UTF8
而不是IndyTextEncoding_UTF16LE
。
確保AData
和AKey
包含有效的輸入數據。
此外,您應確保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()
函數存在很多問題:
您正在泄漏TIdSSLIOHandlerSocketOpenSSL
對象。 您沒有為其分配Owner
,並且TIdHTTP
在分配給其IOHandler
屬性時不會獲得所有權。 事實上,在您的示例中,分配IOHanlder
實際上是可選的,請參閱TIdHTTP的新HTTPS功能 。
您正在將AHTTP.Request.ContentType
設置為錯誤的媒體類型。 您沒有發送XML數據,因此請勿將媒體類型設置為'text/xml'
。 在這種情況下,您需要將其設置為'application/x-www-form-urlencoded'
。
當調用AHTTP.Post()
,您的AStream
流為空,因此您實際上並未將任何數據發布到服務器。 您將AQuery
數據放在URL本身的查詢字符串中,但它實際上屬於AStream
。 如果要在URL查詢字符串中發送數據,則必須使用TIdHTTP.Get()
而不是TIdHTTP.Post()
,並將AMethod
值更改為'GET'
而不是'POST'
。
您正在使用填充輸出TStream
的TIdHTTP.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.