为 Amazon CloudFront 创建签名 URL

Posted

技术标签:

【中文标题】为 Amazon CloudFront 创建签名 URL【英文标题】:Creating Signed URLs for Amazon CloudFront 【发布时间】:2011-02-04 03:49:55 【问题描述】:

短版:如何使用 Python 使用 Amazon CloudFront/S3 来“按需”创建签名 URL 以模仿 nginx 的 X-Accel-Redirect 行为(即保护下载)。

我已经建立了一个 Django 服务器并使用 Nginx 前端运行。我一直收到对它的请求,最近不得不将它安装为Tornado WSGI 应用程序,以防止它在 FastCGI 模式下崩溃。

现在我遇到了一个问题,即我的服务器陷入困境(即它的大部分带宽都被用完了),因为对它的媒体请求太多,我一直在研究 CDN,我相信亚马逊CloudFront/S3 对我来说是合适的解决方案。

我一直在使用 Nginx 的 X-Accel-Redirect 标头来保护文件免遭未经授权的下载,但 CloudFront/S3 没有这种能力——但它们确实提供签名 URL。到目前为止,我还不是 Python 专家,并且绝对不知道如何正确创建签名 URL,所以我希望有人能提供如何使这些 URL “按需”的链接,或者愿意解释如何在这里,将不胜感激。

此外,这是正确的解决方案吗?我对CDN不太熟悉,有没有更适合这个的CDN?

【问题讨论】:

顺便说一句,切换到 uWSGI,你的 FastCGI 就不会再崩溃了。 【参考方案1】:

Amazon CloudFront Signed URLs 与 Amazon S3 签名 URL 的工作方式不同。 CloudFront 使用基于单独 CloudFront 密钥对的 RSA 签名,您必须在您的 Amazon Account Credentials 页面中设置该密钥对。下面是一些使用 M2Crypto 库在 Python 中实际生成限时 URL 的代码:

为 CloudFront 创建密钥对

我认为唯一的方法是通过亚马逊的网站。进入您的 AWS“帐户”页面,然后单击“安全凭证”链接。单击“密钥对”选项卡,然后单击“创建新密钥对”。这将为您生成一个新的密钥对并自动下载一个私钥文件(pk-xxxxxxxxx.pem)。保持密钥文件的安全和私密。还要记下亚马逊的“密钥对 ID”,因为我们将在下一步中使用它。

在 Python 中生成一些 URL

从 boto 2.0 版开始,似乎不支持生成签名的 CloudFront URL。 Python 在标准库中不包含 RSA 加密例程,因此我们将不得不使用额外的库。我在这个例子中使用了 M2Crypto。

对于非流式分发,您必须使用完整的云端 URL 作为资源,但对于流式传输,我们仅使用视频文件的对象名称。有关生成仅持续 5 分钟的 URL 的完整示例,请参见下面的代码。

此代码大致基于 Amazon 在 CloudFront 文档中提供的 php 示例代码。

from M2Crypto import EVP
import base64
import time

def aws_url_base64_encode(msg):
    msg_base64 = base64.b64encode(msg)
    msg_base64 = msg_base64.replace('+', '-')
    msg_base64 = msg_base64.replace('=', '_')
    msg_base64 = msg_base64.replace('/', '~')
    return msg_base64

def sign_string(message, priv_key_string):
    key = EVP.load_key_string(priv_key_string)
    key.reset_context(md='sha1')
    key.sign_init()
    key.sign_update(message)
    signature = key.sign_final()
    return signature

def create_url(url, encoded_signature, key_pair_id, expires):
    signed_url = "%(url)s?Expires=%(expires)s&Signature=%(encoded_signature)s&Key-Pair-Id=%(key_pair_id)s" % 
            'url':url,
            'expires':expires,
            'encoded_signature':encoded_signature,
            'key_pair_id':key_pair_id,
            
    return signed_url

def get_canned_policy_url(url, priv_key_string, key_pair_id, expires):
    #we manually construct this policy string to ensure formatting matches signature
    canned_policy = '"Statement":["Resource":"%(url)s","Condition":"DateLessThan":"AWS:EpochTime":%(expires)s]' % 'url':url, 'expires':expires

    #sign the non-encoded policy
    signature = sign_string(canned_policy, priv_key_string)
    #now base64 encode the signature (URL safe as well)
    encoded_signature = aws_url_base64_encode(signature)

    #combine these into a full url
    signed_url = create_url(url, encoded_signature, key_pair_id, expires);

    return signed_url

def encode_query_param(resource):
    enc = resource
    enc = enc.replace('?', '%3F')
    enc = enc.replace('=', '%3D')
    enc = enc.replace('&', '%26')
    return enc


#Set parameters for URL
key_pair_id = "APKAIAZVIO4BQ" #from the AWS accounts CloudFront tab
priv_key_file = "cloudfront-pk.pem" #your private keypair file
# Use the FULL URL for non-streaming:
resource = "http://34254534.cloudfront.net/video.mp4"
#resource = 'video.mp4' #your resource (just object name for streaming videos)
expires = int(time.time()) + 300 #5 min

#Create the signed URL
priv_key_string = open(priv_key_file).read()
signed_url = get_canned_policy_url(resource, priv_key_string, key_pair_id, expires)

print(signed_url)

#Flash player doesn't like query params so encode them if you're using a streaming distribution
#enc_url = encode_query_param(signed_url)
#print(enc_url)

确保您使用设置为持有您的密钥对的帐户的 TrustedSigners 参数设置您的分配(如果是您自己的帐户,则为“Self”)

请参阅Getting started with secure AWS CloudFront streaming with Python,了解有关使用 Python 进行流式处理的完整示例

【讨论】:

get_canned_policy_url 中,您将编码策略设置为encoded_policy 变量,但从不使用它。那应该在那里吗? 嗨,MattoTodd,你是对的,它不需要在那里。此外,boto v2.1 现在原生支持此功能。请参阅 secretmike.com/2011/10/aws-cloudfront-secure-streaming.html 了解它如何与新的 boto 代码一起使用。一旦 boto 版本发布了一段时间,我会更新这些答案。 有没有使用create_signed_url方法和Boto 2.5.2 ?的例子 @ipegasus - 我在separate answer 中添加了create_signed_url() 的示例,由于@secretmike 努力将他的解决方案整合到boto 中,现在真的非常简单,非常感谢! 【参考方案2】:

正如许多人已经评论过的那样,initially accepted answer 不适用于Amazon CloudFront 实际上,Serving Private Content through CloudFront 需要使用专用的CloudFront Signed URLs - 因此secretmike's answer 是正确的,但同时它已经过时了在他自己花时间和Added support for generating signed URLs for CloudFront(非常感谢!)之后。

boto 现在支持专用的create_signed_url 方法,并且以前的二进制依赖项 M2Crypto 最近也已替换为pure-Python RSA implementation,请参阅Don't use M2Crypto for cloudfront URL signing。

随着越来越普遍,人们可以在相关单元测试中找到一个或多个好的用法示例(请参阅test_signed_urls.py),例如test_canned_policy(self) - 请参阅setUp(self) 以了解引用的变量self.pk_idself.pk_str(显然你需要自己的钥匙):

def test_canned_policy(self):
    """
    Generate signed url from the Example Canned Policy in Amazon's
    documentation.
    """
    url = "http://d604721fxaaqy9.cloudfront.net/horizon.jpg?large=yes&license=yes"
    expire_time = 1258237200
    expected_url = "http://example.com/" # replaced for brevity
    signed_url = self.dist.create_signed_url(
        url, self.pk_id, expire_time, private_key_string=self.pk_str)
    # self.assertEqual(expected_url, signed_url)

【讨论】:

【参考方案3】:

secretmike 的回答有效,但最好使用rsa 而不是M2Crypto

我使用了boto,它使用了rsa

import boto
from boto.cloudfront import CloudFrontConnection
from boto.cloudfront.distribution import Distribution

expire_time = int(time.time() +3000)
conn = CloudFrontConnection('ACCESS_KEY_ID', 'SECRET_ACCESS_KEY')

##enter the id or domain name to select a distribution
distribution = Distribution(connection=conn, config=None, domain_name='', id='', last_modified_time=None, status='')
signed_url = distribution.create_signed_url(url='YOUR_URL', keypair_id='YOUR_KEYPAIR_ID_example-APKAIAZVIO4BQ',expire_time=expire_time,private_key_file="YOUR_PRIVATE_KEY_FILE_LOCATION")

使用boto documentation

【讨论】:

signed_url 中似乎没有导出任何策略。无论如何,我无法让它工作。【参考方案4】:

这就是我用来创建策略的方法,以便我可以访问具有相同“签名”的多个文件:

import json 
import rsa
import time                                                                                                                                                                           

from base64 import b64encode 

url = "http://your_domain/*"                                                                                                                                                                      
expires = int(time.time() + 3600)

pem = """-----BEGIN RSA PRIVATE KEY-----  
...
-----END RSA PRIVATE KEY-----"""

key_pair_id = 'ABX....'

policy =                                                                                                                                                                            
policy['Statement'] = []                                                                                                                                                            
policy['Statement'][0]['Resource'] = url                                                                                                                                              
policy['Statement'][0]['Condition'] =                                                                                                                                               
policy['Statement'][0]['Condition']['DateLessThan'] =                                                                                                                               
policy['Statement'][0]['Condition']['DateLessThan']['AWS:EpochTime'] = expires                                                                                                        

policy = json.dumps(policy)

private_key = rsa.PrivateKey.load_pkcs1(pem)                                                                                                                                          
signature = b64encode(rsa.sign(str(policy), private_key, 'SHA-1'))

print '?Policy=%s&Signature=%s&Key-Pair-Id=%s' % (b64encode(policy),                                                                                                                             
                                                  signature,                                                                                                                          
                                                  key_pair_id)

我可以将它用于http://your_domain/* 下的所有文件,例如:

 http://your_domain/image2.png?Policy...
 http://your_domain/image2.png?Policy...
 http://your_domain/file1.json?Policy...

【讨论】:

【参考方案5】:

这个特性现在是already supported in Botocore,它是Boto3, the latest official AWS SDK for Python的底层库。 (以下示例需要安装 rsa 包,但您也可以使用其他 RSA 包,只需定义自己的“规范化 RSA 签名者”即可。)

用法如下:

    from botocore.signers import CloudFrontSigner
    # First you create a cloudfront signer based on a normalized RSA signer::
    import rsa
    def rsa_signer(message):
        private_key = open('private_key.pem', 'r').read()
        return rsa.sign(
            message,
            rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')),
            'SHA-1')  # CloudFront requires SHA-1 hash
    cf_signer = CloudFrontSigner(key_id, rsa_signer)

    # To sign with a canned policy::
    signed_url = cf_signer.generate_presigned_url(
        url, date_less_than=datetime(2015, 12, 1))

    # To sign with a custom policy::
    signed_url = cf_signer.generate_presigned_url(url, policy=my_policy)

免责声明:我是该 PR 的作者。

【讨论】:

我使用此 API 将 CF 提供的 URL 转换为 CF 签名的 URL,这给了我类似 https://?Expires=1483228800&Signature=&&Key -Pair-Id=my_key_id。但是,该 URL 永不过期。实际的基本 URL https:// 已按原样工作。事实上,我尝试过像 datetime(2015, 12, 1) 这样的旧日期。然而,网址永远不会过期。我在 CF 有什么需要配置的吗? @SoundaR 这是一个不同的问题,该问题更合适的位置是 AWS Boto3 github 问题和/或 Amazon 支持论坛。【参考方案6】:

我发现简单的解决方案不需要更改s3.generate_url 方式,

只需选择您的 Cloudfront 配置:Yes, Update bucket policy

之后从 :

https://xxxx.s3.amazonaws.com/hello.png&Signature=sss&Expires=1585008320&AWSAccessKeyId=kkk

https://yyy.cloudfront.net/hello.png&Signature=sss&Expires=1585008320&AWSAccessKeyId=kkk

yyy.cloudfront.net 是您的 CloudFront 域

参考:https://aws.amazon.com/blogs/developer/accessing-private-content-in-amazon-cloudfront/

【讨论】:

以上是关于为 Amazon CloudFront 创建签名 URL的主要内容,如果未能解决你的问题,请参考以下文章

使用 Ruby 为 CloudFront 创建签名 URL

Amazon Cloudfront 签名 URL 访问被拒绝问题

如何使用预设策略加密 Amazon CloudFront 签名以进行私有内容访问

Amazon CloudFront - 使用签名 URL 保护视频

SignatureDoesNotMatch - Amazon S3 API

Amazon s3 预签名 URL 受 IP 地址限制