litespeed php 不会为亚马逊产品 API 生成正确签名的请求,常规 php 工作正常
litespeed php will not generate a correctly signed request for Amazon product API, regular php works fine
我直接从亚马逊的便签本 (https://webservices.amazon.com/paapi5/scratchpad/index.html) 中获取了下面的 PHP 代码,他们在其中提供样板代码来查询他们的产品 API。我只是稍微修改了一下以显示 HTTP 错误 - 其他一切都是直接复制和粘贴。
我输入了自己的凭据(访问密钥、秘密密钥和合作伙伴标签),代码在我自己的 macOS 笔记本电脑上运行完美,运行(常规)PHP 7.1.23。
我把代码移到我的个人服务器上,运行完美,运行 PHP 5.3.10-1ubuntu3.26
当我将相同的代码移动到客户的服务器时,我收到来自亚马逊的错误消息,指出请求未正确签名。错误是:
{"__type":"com.amazon.paapi5#InvalidSignatureException","Errors":[{"Code":"InvalidSignature","Message":"The request has not been correctly signed. If you are using an AWS SDK, requests are signed for you automatically; otherwise, go to https://webservices.amazon.com/paapi5/documentation/sending-request.html#signing."}]}
在那台机器上,lsphp --version returns
PHP 7.0.27 (litespeed) (built: Jan 4 2018 16:01:06)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
with Zend OPcache v7.0.27, Copyright (c) 1999-2017, by Zend Technologies
直接来自亚马逊的确切代码是:
<?php
/* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
/* Licensed under the Apache License, Version 2.0. */
// Put your Secret Key in place of **********
$serviceName="ProductAdvertisingAPI";
$region="us-east-1";
$accessKey="*****";
$secretKey="*****";
$payload="{"
." \"ItemIds\": ["
." \"0995198918\""
." ],"
." \"Resources\": ["
." \"Images.Primary.Small\","
." \"Images.Primary.Medium\","
." \"Images.Primary.Large\","
." \"Images.Variants.Small\","
." \"Images.Variants.Medium\","
." \"Images.Variants.Large\""
." ],"
." \"PartnerTag\": \"*****\","
." \"PartnerType\": \"Associates\","
." \"Marketplace\": \"www.amazon.com\""
."}";
$host="webservices.amazon.com";
$uriPath="/paapi5/getitems";
$awsv4 = new AwsV4 ($accessKey, $secretKey);
$awsv4->setRegionName($region);
$awsv4->setServiceName($serviceName);
$awsv4->setPath ($uriPath);
$awsv4->setPayload ($payload);
$awsv4->setRequestMethod ("POST");
$awsv4->addHeader ('content-encoding', 'amz-1.0');
$awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
$awsv4->addHeader ('host', $host);
$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems');
$headers = $awsv4->getHeaders ();
$headerString = "";
foreach ( $headers as $key => $value ) {
$headerString .= $key . ': ' . $value . "\r\n";
}
$params = array (
'http' => array (
'ignore_errors' => true,
'header' => $headerString,
'method' => 'POST',
'content' => $payload
)
);
$stream = stream_context_create ( $params );
$fp = fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );
$response = stream_get_contents ( $fp );
echo $response;
if (! $fp) {
throw new Exception ( "Exception Occured" );
}
$response = stream_get_contents ( $fp );
if ($response === false) {
throw new Exception ( "Exception Occured" );
}
echo $response;
class AwsV4 {
private $accessKey = null;
private $secretKey = null;
private $path = null;
private $regionName = null;
private $serviceName = null;
private $httpMethodName = null;
private $queryParametes = array ();
private $awsHeaders = array ();
private $payload = "";
private $HMACAlgorithm = "AWS4-HMAC-SHA256";
private $aws4Request = "aws4_request";
private $strSignedHeader = null;
private $xAmzDate = null;
private $currentDate = null;
public function __construct($accessKey, $secretKey) {
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
$this->xAmzDate = $this->getTimeStamp ();
$this->currentDate = $this->getDate ();
}
function setPath($path) {
$this->path = $path;
}
function setServiceName($serviceName) {
$this->serviceName = $serviceName;
}
function setRegionName($regionName) {
$this->regionName = $regionName;
}
function setPayload($payload) {
$this->payload = $payload;
}
function setRequestMethod($method) {
$this->httpMethodName = $method;
}
function addHeader($headerName, $headerValue) {
$this->awsHeaders [$headerName] = $headerValue;
}
private function prepareCanonicalRequest() {
$canonicalURL = "";
$canonicalURL .= $this->httpMethodName . "\n";
$canonicalURL .= $this->path . "\n" . "\n";
$signedHeaders = '';
foreach ( $this->awsHeaders as $key => $value ) {
$signedHeaders .= $key . ";";
$canonicalURL .= $key . ":" . $value . "\n";
}
$canonicalURL .= "\n";
$this->strSignedHeader = substr ( $signedHeaders, 0, - 1 );
$canonicalURL .= $this->strSignedHeader . "\n";
$canonicalURL .= $this->generateHex ( $this->payload );
return $canonicalURL;
}
private function prepareStringToSign($canonicalURL) {
$stringToSign = '';
$stringToSign .= $this->HMACAlgorithm . "\n";
$stringToSign .= $this->xAmzDate . "\n";
$stringToSign .= $this->currentDate . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "\n";
$stringToSign .= $this->generateHex ( $canonicalURL );
return $stringToSign;
}
private function calculateSignature($stringToSign) {
$signatureKey = $this->getSignatureKey ( $this->secretKey, $this->currentDate, $this->regionName, $this->serviceName );
$signature = hash_hmac ( "sha256", $stringToSign, $signatureKey, true );
$strHexSignature = strtolower ( bin2hex ( $signature ) );
return $strHexSignature;
}
public function getHeaders() {
$this->awsHeaders ['x-amz-date'] = $this->xAmzDate;
ksort ( $this->awsHeaders );
// Step 1: CREATE A CANONICAL REQUEST
$canonicalURL = $this->prepareCanonicalRequest ();
// Step 2: CREATE THE STRING TO SIGN
$stringToSign = $this->prepareStringToSign ( $canonicalURL );
// Step 3: CALCULATE THE SIGNATURE
$signature = $this->calculateSignature ( $stringToSign );
// Step 4: CALCULATE AUTHORIZATION HEADER
if ($signature) {
$this->awsHeaders ['Authorization'] = $this->buildAuthorizationString ( $signature );
return $this->awsHeaders;
}
}
private function buildAuthorizationString($strSignature) {
return $this->HMACAlgorithm . " " . "Credential=" . $this->accessKey . "/" . $this->getDate () . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "," . "SignedHeaders=" . $this->strSignedHeader . "," . "Signature=" . $strSignature;
}
private function generateHex($data) {
return strtolower ( bin2hex ( hash ( "sha256", $data, true ) ) );
}
private function getSignatureKey($key, $date, $regionName, $serviceName) {
$kSecret = "AWS4" . $key;
$kDate = hash_hmac ( "sha256", $date, $kSecret, true );
$kRegion = hash_hmac ( "sha256", $regionName, $kDate, true );
$kService = hash_hmac ( "sha256", $serviceName, $kRegion, true );
$kSigning = hash_hmac ( "sha256", $this->aws4Request, $kService, true );
return $kSigning;
}
private function getTimeStamp() {
return gmdate ( "Ymd\THis\Z" );
}
private function getDate() {
return gmdate ( "Ymd" );
}
}
?>
lsphp 可执行文件有何不同之处阻止它生成正确签名的请求?
正如@esqew 所建议的那样,服务器上的时间偏移了大约 5 分钟,这导致 GMT 时间也偏移了,然后我猜亚马逊不喜欢这个请求。
一旦我们调整示例亚马逊代码完美运行,一切都很好!
非常感谢,
如评论中所述,如果服务器时间与实际当前时间相差很大,哈希函数将产生亚马逊将拒绝的错误结果。
重新配置服务器以使用网络同步其时钟,或手动调整以反映正确的时间,以便 PHP 的 date/time 功能开始返回正确的时间。
我直接从亚马逊的便签本 (https://webservices.amazon.com/paapi5/scratchpad/index.html) 中获取了下面的 PHP 代码,他们在其中提供样板代码来查询他们的产品 API。我只是稍微修改了一下以显示 HTTP 错误 - 其他一切都是直接复制和粘贴。
我输入了自己的凭据(访问密钥、秘密密钥和合作伙伴标签),代码在我自己的 macOS 笔记本电脑上运行完美,运行(常规)PHP 7.1.23。
我把代码移到我的个人服务器上,运行完美,运行 PHP 5.3.10-1ubuntu3.26
当我将相同的代码移动到客户的服务器时,我收到来自亚马逊的错误消息,指出请求未正确签名。错误是:
{"__type":"com.amazon.paapi5#InvalidSignatureException","Errors":[{"Code":"InvalidSignature","Message":"The request has not been correctly signed. If you are using an AWS SDK, requests are signed for you automatically; otherwise, go to https://webservices.amazon.com/paapi5/documentation/sending-request.html#signing."}]}
在那台机器上,lsphp --version returns
PHP 7.0.27 (litespeed) (built: Jan 4 2018 16:01:06)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
with Zend OPcache v7.0.27, Copyright (c) 1999-2017, by Zend Technologies
直接来自亚马逊的确切代码是:
<?php
/* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
/* Licensed under the Apache License, Version 2.0. */
// Put your Secret Key in place of **********
$serviceName="ProductAdvertisingAPI";
$region="us-east-1";
$accessKey="*****";
$secretKey="*****";
$payload="{"
." \"ItemIds\": ["
." \"0995198918\""
." ],"
." \"Resources\": ["
." \"Images.Primary.Small\","
." \"Images.Primary.Medium\","
." \"Images.Primary.Large\","
." \"Images.Variants.Small\","
." \"Images.Variants.Medium\","
." \"Images.Variants.Large\""
." ],"
." \"PartnerTag\": \"*****\","
." \"PartnerType\": \"Associates\","
." \"Marketplace\": \"www.amazon.com\""
."}";
$host="webservices.amazon.com";
$uriPath="/paapi5/getitems";
$awsv4 = new AwsV4 ($accessKey, $secretKey);
$awsv4->setRegionName($region);
$awsv4->setServiceName($serviceName);
$awsv4->setPath ($uriPath);
$awsv4->setPayload ($payload);
$awsv4->setRequestMethod ("POST");
$awsv4->addHeader ('content-encoding', 'amz-1.0');
$awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
$awsv4->addHeader ('host', $host);
$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems');
$headers = $awsv4->getHeaders ();
$headerString = "";
foreach ( $headers as $key => $value ) {
$headerString .= $key . ': ' . $value . "\r\n";
}
$params = array (
'http' => array (
'ignore_errors' => true,
'header' => $headerString,
'method' => 'POST',
'content' => $payload
)
);
$stream = stream_context_create ( $params );
$fp = fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );
$response = stream_get_contents ( $fp );
echo $response;
if (! $fp) {
throw new Exception ( "Exception Occured" );
}
$response = stream_get_contents ( $fp );
if ($response === false) {
throw new Exception ( "Exception Occured" );
}
echo $response;
class AwsV4 {
private $accessKey = null;
private $secretKey = null;
private $path = null;
private $regionName = null;
private $serviceName = null;
private $httpMethodName = null;
private $queryParametes = array ();
private $awsHeaders = array ();
private $payload = "";
private $HMACAlgorithm = "AWS4-HMAC-SHA256";
private $aws4Request = "aws4_request";
private $strSignedHeader = null;
private $xAmzDate = null;
private $currentDate = null;
public function __construct($accessKey, $secretKey) {
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
$this->xAmzDate = $this->getTimeStamp ();
$this->currentDate = $this->getDate ();
}
function setPath($path) {
$this->path = $path;
}
function setServiceName($serviceName) {
$this->serviceName = $serviceName;
}
function setRegionName($regionName) {
$this->regionName = $regionName;
}
function setPayload($payload) {
$this->payload = $payload;
}
function setRequestMethod($method) {
$this->httpMethodName = $method;
}
function addHeader($headerName, $headerValue) {
$this->awsHeaders [$headerName] = $headerValue;
}
private function prepareCanonicalRequest() {
$canonicalURL = "";
$canonicalURL .= $this->httpMethodName . "\n";
$canonicalURL .= $this->path . "\n" . "\n";
$signedHeaders = '';
foreach ( $this->awsHeaders as $key => $value ) {
$signedHeaders .= $key . ";";
$canonicalURL .= $key . ":" . $value . "\n";
}
$canonicalURL .= "\n";
$this->strSignedHeader = substr ( $signedHeaders, 0, - 1 );
$canonicalURL .= $this->strSignedHeader . "\n";
$canonicalURL .= $this->generateHex ( $this->payload );
return $canonicalURL;
}
private function prepareStringToSign($canonicalURL) {
$stringToSign = '';
$stringToSign .= $this->HMACAlgorithm . "\n";
$stringToSign .= $this->xAmzDate . "\n";
$stringToSign .= $this->currentDate . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "\n";
$stringToSign .= $this->generateHex ( $canonicalURL );
return $stringToSign;
}
private function calculateSignature($stringToSign) {
$signatureKey = $this->getSignatureKey ( $this->secretKey, $this->currentDate, $this->regionName, $this->serviceName );
$signature = hash_hmac ( "sha256", $stringToSign, $signatureKey, true );
$strHexSignature = strtolower ( bin2hex ( $signature ) );
return $strHexSignature;
}
public function getHeaders() {
$this->awsHeaders ['x-amz-date'] = $this->xAmzDate;
ksort ( $this->awsHeaders );
// Step 1: CREATE A CANONICAL REQUEST
$canonicalURL = $this->prepareCanonicalRequest ();
// Step 2: CREATE THE STRING TO SIGN
$stringToSign = $this->prepareStringToSign ( $canonicalURL );
// Step 3: CALCULATE THE SIGNATURE
$signature = $this->calculateSignature ( $stringToSign );
// Step 4: CALCULATE AUTHORIZATION HEADER
if ($signature) {
$this->awsHeaders ['Authorization'] = $this->buildAuthorizationString ( $signature );
return $this->awsHeaders;
}
}
private function buildAuthorizationString($strSignature) {
return $this->HMACAlgorithm . " " . "Credential=" . $this->accessKey . "/" . $this->getDate () . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "," . "SignedHeaders=" . $this->strSignedHeader . "," . "Signature=" . $strSignature;
}
private function generateHex($data) {
return strtolower ( bin2hex ( hash ( "sha256", $data, true ) ) );
}
private function getSignatureKey($key, $date, $regionName, $serviceName) {
$kSecret = "AWS4" . $key;
$kDate = hash_hmac ( "sha256", $date, $kSecret, true );
$kRegion = hash_hmac ( "sha256", $regionName, $kDate, true );
$kService = hash_hmac ( "sha256", $serviceName, $kRegion, true );
$kSigning = hash_hmac ( "sha256", $this->aws4Request, $kService, true );
return $kSigning;
}
private function getTimeStamp() {
return gmdate ( "Ymd\THis\Z" );
}
private function getDate() {
return gmdate ( "Ymd" );
}
}
?>
lsphp 可执行文件有何不同之处阻止它生成正确签名的请求?
正如@esqew 所建议的那样,服务器上的时间偏移了大约 5 分钟,这导致 GMT 时间也偏移了,然后我猜亚马逊不喜欢这个请求。
一旦我们调整示例亚马逊代码完美运行,一切都很好!
非常感谢,
如评论中所述,如果服务器时间与实际当前时间相差很大,哈希函数将产生亚马逊将拒绝的错误结果。
重新配置服务器以使用网络同步其时钟,或手动调整以反映正确的时间,以便 PHP 的 date/time 功能开始返回正确的时间。