使用S3签名网址时签名不匹配

时间:2016-05-21 12:39:25

标签: php amazon-s3

我在S3中有一个存储桶,我将其链接到CNAME别名。我们现在假设该域名为 media.mycompany.com 。在存储桶中是所有设置为私有的图像文件。然而,他们使用URL签名在我的网站上公开使用。签名的URL可能如下所示:

http://media.mycompany.com/images/651/38935_small.JPG?AWSAccessKeyId=05GMT0V3GWVNE7GGM1R2&Expires=1466035210&Signature=Uk3K7qVNRFHuIUnMaDadCOPjV%2BM%3D

这样可以正常工作。我在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);

这将返回此帖子前面共享的正确签名的URL,一切都很好。

但是,我现在想生成HTTPS URL,这就是我遇到问题的地方。只需将HTTP添加到当前URL(通过将方法的最后一个参数设置为true)将无效,它将生成如下URL:

https://media.mycompany.com/images/651/38935_small.JPG?AWSAccessKeyId=05GMT0V3GWVNE7GGM1R2&Expires=1466035210&Signature=Uk3K7qVNRFHuIUnMaDadCOPjV%2BM%3D

这显然不起作用,因为我的SSL证书(由letsencrypt创建)未安装在亚马逊的域上,据我所知,没有办法这样做。

我已经了解了通过SSL访问存储桶的替代网址格式:

https://media.mycompany.com.s3.amazonaws.com/images/651/38935_small.JPG?AWSAccessKeyId=05GMT0V3GWVNE7GGM1R2&Expires=1466035210&Signature=Uk3K7qVNRFHuIUnMaDadCOPjV%2BM%3D

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

最后,有这种格式:

https://s3.amazonaws.com/media.mycompany.com/images/2428/39000_small.jpg?AWSAccessKeyId=05GMT0V3GWVNE7GGM1R2&Expires=1466035210&Signature=6p3W6GHQtddJNnCoUXaNl970x9s%3D

在这里,我越来越近了。如果我采用有效的非安全URL,并编辑URL以采用此格式,则可行。显示图像。

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

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

此处的更改是备用存储桶名称格式,最后一个参数设置为true,表示HTTP。这导致了这样的输出:

https://s3.amazonaws.com/media.mycompany.com/images/2784/38965_small.jpg?AWSAccessKeyId=05GMT0V3GWVNE7GGM1R2&Expires=1466035210&Signature=Db2ynwWOV852Mn4rpcWA0Q1DrH0%3D

如您所见,它的格式与我手动制作的网址格式相同。但不幸的是,我收到了签名错误:

<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your key and signing method.
</Message>

我很难搞清楚为什么这些签名不正确。我尝试将签名方法的第4个参数设置为true和false,但没有区别。

我错过了什么?

修改

根据迈克尔的回答,我试图在调用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);

2 个答案:

答案 0 :(得分:2)

  

此处的更改是备用存储桶名称格式

几乎。这个库似乎没有你需要的东西,以便你做你想做的事。

对于签名版本2(您正在使用的版本),最简单的解决方法是使用https://bucket.s3.amazonaws.com/path签名的网址,并将字符串替换为https://s3.amazonaws.com/bucket/path.¹这是有效的,因为签名在V2中是等效的。它不适用于Signature V4,但您没有使用它。

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

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

如果您想隐藏URL中的“S3”并使用您自己的SSL证书,可以在S3前面使用CloudFront来完成。使用CloudFront,您可以使用自己的证书,并将其指向任何存储桶,无论存储桶名称是否与原始主机名匹配。但是,CloudFront对签名URL使用非常差异的算法,因此您需要代码来支持它。 CloudFront签名URL的一个优点 - 可能对您有用,也可能没有用 - 是您可以生成一个签名URL,该URL只能使用您在签名策略中包含的特定IP地址。

还可以使用特殊配置的CloudFront传递已签名的S3 URL(将存储桶配置为自定义源,而不是S3源,并将查询字符串转发到源)但这会破坏CloudFront中的所有缓存,因此这有点适得其反......但它会起作用。

¹请注意,在这样重写时必须使用区域端点,除非您的存储桶位于us-east-1(又称美国标准)中,因此对于美国西部的存储区,主机名将为s3-us-west-2.amazonaws.com例如-2。对于美国标准,s3.amazonaws.coms3-external-1.amazonaws.com可以与https网址一起使用。

答案 1 :(得分:0)

花了几天时间试图为预签名 URL 设置自定义 CNAME/主机,但似乎不可能。

所有论坛都说这无法完成,或者您必须重新编码整个应用程序才能改用 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";

        } 
    }