Php 使用S3签名URL时签名不匹配

Php 使用S3签名URL时签名不匹配,php,amazon-s3,Php,Amazon S3,我在S3中有一个bucket,它链接到一个CNAME别名。现在让我们假设域是media.mycompany.com。在bucket中是全部设置为private的图像文件。然而,他们在我的网站上公开使用URL签名。签名URL可能如下所示: $return = $this->s3->getAuthenticatedURL('media.mycompany.com', $dir . '/' . $filename, $timestamp, true,

我在S3中有一个bucket,它链接到一个CNAME别名。现在让我们假设域是media.mycompany.com。在bucket中是全部设置为private的图像文件。然而,他们在我的网站上公开使用URL签名。签名URL可能如下所示:

$return = $this->s3->getAuthenticatedURL('media.mycompany.com', $dir . '/' . $filename,
                    $timestamp, true, false);
$return = $this->s3->getAuthenticatedURL("s3.amazonaws.com/media.mycompany.com", $dir . '/' . $filename,
                    $timestamp, true, true);

这样做很好。我正在使用PHP中的S3帮助程序库来生成这样的URL。以下是该库的标识符:

$Id: S3.php 44 2008-12-23 15:38:38Z don.schonknecht $
我知道它是旧的,但我在这个库中依赖很多方法,所以升级并不琐碎,正如前面所说,它对我很好地工作。以下是此库中的相关方法:

public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) {
    $expires = time() + $lifetime;
    $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea)
    return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
    $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires,
    urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
}
在我的正常工作设置中,我会这样调用此方法:

$return = $this->s3->getAuthenticatedURL('media.mycompany.com', $dir . '/' . $filename,
                    $timestamp, true, false);
$return = $this->s3->getAuthenticatedURL("s3.amazonaws.com/media.mycompany.com", $dir . '/' . $filename,
                    $timestamp, true, true);
这将返回本文前面共享的正确签名的URL,一切正常

然而,我现在想要生成HTTPS URL,这就是我遇到问题的地方。简单地将HTTPs添加到当前URL(通过将方法的最后一个参数设置为true)将不起作用,它将生成如下URL:

$return = $this->s3->getAuthenticatedURL('media.mycompany.com', $dir . '/' . $filename,
                    $timestamp, true, false);
$return = $this->s3->getAuthenticatedURL("s3.amazonaws.com/media.mycompany.com", $dir . '/' . $filename,
                    $timestamp, true, true);

这显然行不通,因为我的SSL证书(由letsencrypt创建)没有安装在Amazon的域上,据我所知,没有办法这样做

我已经了解了通过SSL访问bucket的另一种URL格式:

这显然适用于某些人,但不适用于我,据我所知,这是因为我的桶名中有一个点(.)字符。我无法更改bucket名称,这将在我的设置中产生重大影响

最后,还有一种格式:

我离这里越来越近了。如果我使用一个不安全的工作URL,并编辑该URL以采用这种格式,它就可以工作。如图所示

现在我想让它以自动化的方式工作,从我前面展示的签名方法开始。我这样称呼它:

$return = $this->s3->getAuthenticatedURL('media.mycompany.com', $dir . '/' . $filename,
                    $timestamp, true, false);
$return = $this->s3->getAuthenticatedURL("s3.amazonaws.com/media.mycompany.com", $dir . '/' . $filename,
                    $timestamp, true, true);
这里的更改是可选的bucket name格式,最后一个参数设置为true,表示HTTPs。这将导致如下输出:

$return = $this->s3->getAuthenticatedURL('media.mycompany.com', $dir . '/' . $filename,
                    $timestamp, true, false);
$return = $this->s3->getAuthenticatedURL("s3.amazonaws.com/media.mycompany.com", $dir . '/' . $filename,
                    $timestamp, true, true);

如您所见,它的格式与我手工创建的URL相同。但不幸的是,我收到了签名错误:

<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your key and signing method.
</Message>
我一直在想为什么这些签名不正确。我尝试将signing方法的第4个参数设置为true和false,但没有任何区别

我错过了什么

编辑

根据Michael在下面的回答,在调用S3库之后,我尝试进行简单的字符串替换,这很有效。快速脏代码:

                $return = $this->s3->getAuthenticatedURL("media.mycompany.com", $dir . '/' . $filename, $timestamp, true, true);
                $return = substr_replace($return, "s3.amazonaws.com/", strpos($return, "media.mycompany.com"), 0);
这里的更改是可选的bucket名称格式

差不多。这个库似乎没有您想要做的事情

对于签名版本2(您正在使用),最简单的解决方法是使用
https://bucket.s3.amazonaws.com/path
只需将字符串替换为
https://s3.amazonaws.com/bucket/path
。由于签名在V2中是等效的,所以这是有效的。它对签名V4不起作用,但您没有使用它

或者您需要重写支持库中的代码,以使用路径样式URL的另一个选项来处理这种情况

“hostbucket”选项似乎假定一个以bucket命名的CNAME或别名指向S3端点,这不适用于HTTPS。将此选项设置为true实际上会导致库对名为
s3.amazonaws.com/media.example.com
的bucket的URL进行签名,这就是签名不匹配的原因

如果您想从URL中隐藏“S3”并使用自己的SSL证书,可以在S3前面使用CloudFront。使用CloudFront,您可以使用自己的证书,并将其指向任何bucket,而不管bucket名称是否与原始主机名匹配。然而,CloudFront对签名URL使用了一种非常不同的算法,所以您需要代码来支持它。CloudFront签名URL的一个优点是,您可以生成一个签名URL,该URL只能从签名策略中包含的特定IP地址工作,这可能对您有用,也可能对您不有用

也可以通过CloudFront的特殊配置来传递签名的S3URL(将bucket配置为自定义源,而不是S3源,并将查询字符串转发到源),但这会破坏CloudFront中的所有缓存,因此有点事与愿违。。。但这是可行的



⑨请注意,当您这样重写时,必须使用区域端点,除非您的存储桶位于us-east-1(又称美国标准)中,例如,对于us-west-2中的存储桶,主机名应为
s3-us-west-2.amazonaws.com
。对于美国标准,无论是
s3.amazonaws.com
还是
s3-external-1.amazonaws.com
都可以与https URL一起使用。

花了好几天的时间绕圈尝试为预先指定的URL设置自定义CNAME/host,这似乎是不可能的

所有论坛都说这是不可能的,或者你必须重新编码你的整个应用程序来使用cloudfront

将我的DNS从MYBUCKET.s3-WEBSITE-eu-west-1.amazonaws.com更改为MYBUCKET.s3-eu-west-1.amazonaws.com,立即修复了此问题

希望这能帮助别人

工作代码:

function get_objectURL($key) {


        // Instantiate the client.
        $this->s3 = S3Client::factory(array(
            'credentials' => array(
                'key'    => s3_key,
                'secret' => s3_secret,
            ),
            'region'  => 'eu-west-1',
            'version' => 'latest',
            'endpoint' => 'https://example.com',
            'bucket_endpoint' => true,
            'signature_version' => 'v4'
        ));


        $cmd = $this->s3->getCommand('GetObject', [
            'Bucket' => s3_bucket,
            'Key' => $key
        ]);


        try {
        
            $request = $this->s3->createPresignedRequest($cmd, '+5 minutes');

            // Get the actual presigned-url
            $presignedUrl = (string)$request->getUri();

            return $presignedUrl;

        } catch (S3Exception $e) {

            return $e->getMessage() . "\n";

        } 
    }

你是个英雄。事实上,这个库根本无法处理所需的格式,而您快速重写字符串的想法很有帮助。我对原始问题进行了编辑,以包含被证明有效的快速而肮脏的代码。