为Amazon CloudFront创建签名URL

32

简短版:如何使用Python在Amazon CloudFront/S3上生成“按需”的带签名URL以模仿Nginx的X-Accel-Redirect行为(即保护下载)。

我已经部署了一个Django服务器,并通过Nginx前端进行了配置。由于请求太多,最近不得不将其安装为Tornado WSGI应用程序,以防止在FastCGI模式下崩溃。

现在我的服务器存在一个问题,它因为有太多的媒体请求而变得非常拥塞(即大部分带宽被使用),我一直在研究CDN并且相信Amazon CloudFront/S3将是我正确的解决方案。

我一直在使用Nginx的X-Accel-Redirect头来保护文件免受未经授权的下载,但是我无法在CloudFront/S3上做到这一点--然而他们提供了签名URL。我远非Python专家,也不知道如何正确地创建签名URL,因此我希望有人能够提供如何生成这些“按需”URL的链接或愿意在这里解释如何操作,我将非常感激。

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


顺便说一下,切换到uWSGI后,你的FastCGI就不会再崩溃了。 - Peter Bengtsson
6个回答

36
亚马逊云前缀签名URL和亚马逊S3签名URL工作方式不同。 CloudFront使用基于单独的CloudFront密钥对的RSA签名,您必须在Amazon帐户凭据页中设置它。这是一些Python代码,可使用 M2Crypto 库实际生成有时限的URL:

为CloudFront创建密钥对

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

在Python中生成一些URL

截至boto版本2.0,似乎没有支持生成已签名CloudFront URL的功能。 Python未在标准库中包含RSA加密例程,因此我们必须使用其他库。 我在此示例中使用了M2Crypto。

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

此代码松散基于亚马逊在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”)

请参阅《使用Python开始安全的AWS CloudFront流媒体》,了解如何在Python中进行全面的设置示例。


get_canned_policy_url函数内,您将编码策略设置为encoded_policy变量,但从未使用过它。那应该存在那里吗? - MattoTodd
2
嗨MattoTodd,你是对的,那个不需要在那里。此外,boto v2.1现在本地支持这个功能。请参见http://www.secretmike.com/2011/10/aws-cloudfront-secure-streaming.html以了解新的boto代码如何工作。一旦boto版本发布一段时间后,我将更新这些答案。 - secretmike
1
有没有使用 create_signed_url 方法和 Boto 2.5.2 的示例? - ipegasus
1
@ipegasus - 我在单独的答案中添加了一个create_signed_url()的示例,由于@secretmike将他的解决方案纳入boto中,现在它非常简单,非常感谢! - Steffen Opel

29
这个功能现在已经在Botocore中得到了支持, 它是Boto3的基础库,最新的官方AWS Python SDK。 (以下示例需要安装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://<mycf.cloudfront.net/<my_s3_bucket_obj>?Expires=1483228800&Signature=<signature>&&Key-Pair-Id=my_key_id。然而,该URL永不过期。实际的基本URL https://<mycf.cloudfront.net/<my_s3_bucket_obj> 已经可以正常工作。事实上,我尝试了旧日期,如datetime(2015, 12, 1),但URL仍然没有过期。我是否遗漏了在CF上配置的任何内容? - RajSoundar
@SoundaR 那是一个不同的问题,更合适的地方是在 AWS Boto3 的 GitHub 问题页面和/或亚马逊支持论坛提问。 - RayLuo

16

正如许多人已经评论的那样,最初被接受的答案实际上并不适用于Amazon CloudFront,事实上,通过CloudFront提供私有内容需要使用专用的CloudFront签名URL - 因此,secretmike的回答是正确的,但在他自己花时间并且添加了支持为CloudFront生成签名URL之后,现在已过时(非常感谢这个!)。

boto现在支持一个专用的create_signed_url方法,而以前的二进制依赖包M2Crypto也最近被替换为纯Python RSA实现,请参见不要使用M2Crypto进行cloudfront URL签名

随着越来越普遍的情况,可以在相关的单元测试中找到一个或多个很好的用法示例(请参见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)

1
这是我用来创建策略的方法,以便我可以为具有相同“签名”的多个文件授予访问权限:
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...

0

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

只需选择您的Cloudfront配置:是,更新存储桶策略

之后从:

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/

enter image description here


0

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")

请使用{{link1:boto文档}}


在signed_url中似乎没有导出任何策略。无论如何,我都无法让它工作。 - user2105469

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接