使用Python开始使用安全的AWS CloudFront流媒体

时间:2011-07-01 15:10:29

标签: python amazon-s3 amazon-web-services amazon-cloudfront

我创建了一个S3存储桶,上传了一个视频,在CloudFront中创建了一个流媒体分发。用静态HTML播放器测试它,它的工作原理。我已通过帐户设置创建了密钥对。我目前在桌面上有私钥文件。那就是我。

我的目标是达到我的Django / Python网站创建安全网址的程度,除非他们来自我的某个网页,否则他们无法访问这些视频。问题是我对亚马逊摆脱困境的方式过敏,而且我变得越来越困惑。

我意识到这不会是StackOverflow上最好的问题,但我确信我不能成为这里唯一一个无法在如何设置安全的CloudFront / S3的情况。我真的很感谢你的帮助,并且愿意(一旦两天过去)给予最好的答案500pt恩惠。

我有几个问题,一旦回答,应该符合如何完成我所追求的一个解释:

  • 在文档中(下一个例子中有一个例子)有很多XML,告诉我需要POST到各个地方的东西。这样做是否有在线控制台?或者我真的必须通过cURL(等)强制执行此操作?

  • 如何为CloudFront创建Origin Access Identity并将其绑定到我的发行版?我读过this document但是,根据第一点,不知道如何处理它。我的密钥对如何适合这个?

  • 一旦完成,我如何限制S3存储桶只允许人们通过该身份下载内容?如果这是另一个XML作业,而不是点击网页用户界面,请告诉我应该在哪里以及如何将其纳入我的帐户。

  • 在Python中,为文件生成过期URL的最简单方法是什么。我安装了boto,但我没看到如何从流媒体发布中获取文件。

  • 是否有任何应用程序或脚本可能难以设置此服装?我使用Ubuntu(Linux),但如果它只是Windows,我在VM中有XP。我已经看过CloudBerry S3 Explorer Pro了 - 但它与在线用户界面一样有意义。

2 个答案:

答案 0 :(得分:53)

你说得对,需要大量的API才能完成这项工作。我希望他们很快就能在AWS控制台中获得它!

更新:我已将此代码提交给boto - 从boto v2.1(2011-10-27发布)开始,这变得更加容易。对于boto< 2.1,使用此处的说明。对于boto 2.1或更高版本,请在我的博客上获取更新说明:http://www.secretmike.com/2011/10/aws-cloudfront-secure-streaming.html一旦boto v2.1被更多发行版打包,我将在此处更新答案。

要完成您想要的任务,您需要执行以下步骤,我将在下面详细说明:

  1. 创建你的s3存储桶并上传一些对象(你已经完成了这个)
  2. 创建Cloudfront“Origin Access Identity”(基本上是一个AWS账户,允许Cloudfront访问您的s3存储桶)
  3. 修改对象上的ACL,以便只允许您的Cloudfront Origin Access Identity读取它们(这可以防止人们绕过Cloudfront并直接转到s3)
  4. 使用基本网址创建云端分发,并使用需要签名的网址
  5. 测试您是否可以从基本的云端分发版下载对象,但不能从s3或签名的云端分发版下载
  6. 为签名网址创建密钥对
  7. 使用Python生成一些网址
  8. 测试已签名的网址是否有效

  9. 1 - 创建广告素材和上传对象

    最简单的方法是通过AWS控制台,但为了完整起见,我将展示如何使用boto。 Boto代码如下所示:

    import boto
    
    #credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    s3 = boto.connect_s3()
    
    #bucket name MUST follow dns guidelines
    new_bucket_name = "stream.example.com"
    bucket = s3.create_bucket(new_bucket_name)
    
    object_name = "video.mp4"
    key = bucket.new_key(object_name)
    key.set_contents_from_filename(object_name)
    

    2 - 创建Cloudfront“Origin Access Identity”

    目前,此步骤只能使用API​​执行。 Boto代码在这里:

    import boto
    
    #credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    cf = boto.connect_cloudfront()
    
    oai = cf.create_origin_access_identity(comment='New identity for secure videos')
    
    #We need the following two values for later steps:
    print("Origin Access Identity ID: %s" % oai.id)
    print("Origin Access Identity S3CanonicalUserId: %s" % oai.s3_user_id)
    

    3 - 修改对象的ACL

    现在我们已经拥有了特殊的S3用户帐户(我们上面创建的S3CanonicalUserId),我们需要让它访问我们的s3对象。我们可以通过打开对象的(而不是存储桶的!)权限选项卡,单击“添加更多权限”按钮,并将上面提到的很长的S3CanonicalUserId粘贴到新的“Grantee”字段中,轻松地使用AWS控制台执行此操作。确保您授予新的“打开/下载”权限。

    您也可以使用以下boto脚本在代码中执行此操作:

    import boto
    
    #credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    s3 = boto.connect_s3()
    
    bucket_name = "stream.example.com"
    bucket = s3.get_bucket(bucket_name)
    
    object_name = "video.mp4"
    key = bucket.get_key(object_name)
    
    #Now add read permission to our new s3 account
    s3_canonical_user_id = "<your S3CanonicalUserID from above>"
    key.add_user_grant("READ", s3_canonical_user_id)
    

    4 - 创建云端分发

    请注意,在编写本文时尚未正式发布的2.0版本之前,boto不完全支持自定义源和私有发行版。下面的代码从boto 2.0分支中提取出一些代码,然后将它们组合在一起以实现它,但它并不漂亮。 2.0分支处理得更加优雅 - 如果可能的话绝对使用它!

    import boto
    from boto.cloudfront.distribution import DistributionConfig
    from boto.cloudfront.exception import CloudFrontServerError
    
    import re
    
    def get_domain_from_xml(xml):
        results = re.findall("<DomainName>([^<]+)</DomainName>", xml)
        return results[0]
    
    #custom class to hack this until boto v2.0 is released
    class HackedStreamingDistributionConfig(DistributionConfig):
    
        def __init__(self, connection=None, origin='', enabled=False,
                     caller_reference='', cnames=None, comment='',
                     trusted_signers=None):
            DistributionConfig.__init__(self, connection=connection,
                                        origin=origin, enabled=enabled,
                                        caller_reference=caller_reference,
                                        cnames=cnames, comment=comment,
                                        trusted_signers=trusted_signers)
    
        #override the to_xml() function
        def to_xml(self):
            s = '<?xml version="1.0" encoding="UTF-8"?>\n'
            s += '<StreamingDistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">\n'
    
            s += '  <S3Origin>\n'
            s += '    <DNSName>%s</DNSName>\n' % self.origin
            if self.origin_access_identity:
                val = self.origin_access_identity
                s += '    <OriginAccessIdentity>origin-access-identity/cloudfront/%s</OriginAccessIdentity>\n' % val
            s += '  </S3Origin>\n'
    
    
            s += '  <CallerReference>%s</CallerReference>\n' % self.caller_reference
            for cname in self.cnames:
                s += '  <CNAME>%s</CNAME>\n' % cname
            if self.comment:
                s += '  <Comment>%s</Comment>\n' % self.comment
            s += '  <Enabled>'
            if self.enabled:
                s += 'true'
            else:
                s += 'false'
            s += '</Enabled>\n'
            if self.trusted_signers:
                s += '<TrustedSigners>\n'
                for signer in self.trusted_signers:
                    if signer == 'Self':
                        s += '  <Self/>\n'
                    else:
                        s += '  <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer
                s += '</TrustedSigners>\n'
            if self.logging:
                s += '<Logging>\n'
                s += '  <Bucket>%s</Bucket>\n' % self.logging.bucket
                s += '  <Prefix>%s</Prefix>\n' % self.logging.prefix
                s += '</Logging>\n'
            s += '</StreamingDistributionConfig>\n'
    
            return s
    
        def create(self):
            response = self.connection.make_request('POST',
                '/%s/%s' % ("2010-11-01", "streaming-distribution"),
                {'Content-Type' : 'text/xml'},
                data=self.to_xml())
    
            body = response.read()
            if response.status == 201:
                return body
            else:
                raise CloudFrontServerError(response.status, response.reason, body)
    
    
    cf = boto.connect_cloudfront()
    
    s3_dns_name = "stream.example.com.s3.amazonaws.com"
    comment = "example streaming distribution"
    oai = "<OAI ID from step 2 above like E23KRHS6GDUF5L>"
    
    #Create a distribution that does NOT need signed URLS
    hsd = HackedStreamingDistributionConfig(connection=cf, origin=s3_dns_name, comment=comment, enabled=True)
    hsd.origin_access_identity = oai
    basic_dist = hsd.create()
    print("Distribution with basic URLs: %s" % get_domain_from_xml(basic_dist))
    
    #Create a distribution that DOES need signed URLS
    hsd = HackedStreamingDistributionConfig(connection=cf, origin=s3_dns_name, comment=comment, enabled=True)
    hsd.origin_access_identity = oai
    #Add some required signers (Self means your own account)
    hsd.trusted_signers = ['Self']
    signed_dist = hsd.create()
    print("Distribution with signed URLs: %s" % get_domain_from_xml(signed_dist))
    

    5 - 测试您可以从cloudfront下载对象但不能从s3下载

    您现在应该可以验证:

    • stream.example.com.s3.amazonaws.com/video.mp4 - 应该提供AccessDenied
    • signed_distribution.cloudfront.net/video.mp4 - 应该给MissingKey(因为URL未签名)
    • basic_distribution.cloudfront.net/video.mp4 - 应该可以正常使用

    必须调整测试以与您的流播放器一起使用,但基本思路是只有基本的云端网址才能正常工作。

    6 - 为CloudFront创建密钥对

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

    7 - 在Python中生成一些网址

    从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(str(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}
    
        #now base64 encode it (must be URL safe)
        encoded_policy = aws_url_base64_encode(canned_policy)
        #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 = "APKAIAZCZRKVIO4BQ" #from the AWS accounts page
    priv_key_file = "cloudfront-pk.pem" #your private keypair file
    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)
    
    #Flash player doesn't like query params so encode them
    enc_url = encode_query_param(signed_url)
    print(enc_url)
    

    8 - 试用网址

    希望你现在应该有一个看起来像这样的工作URL:

    video.mp4%3FExpires%3D1309979985%26Signature%3DMUNF7pw1689FhMeSN6JzQmWNVxcaIE9mk1x~KOudJky7anTuX0oAgL~1GW-ON6Zh5NFLBoocX3fUhmC9FusAHtJUzWyJVZLzYT9iLyoyfWMsm2ylCDBqpy5IynFbi8CUajd~CjYdxZBWpxTsPO3yIFNJI~R2AFpWx8qp3fs38Yw_%26Key-Pair-Id%3DAPKAIAZRKVIO4BQ
    

    将它放入你的js中你应该有类似的东西(来自亚马逊CloudFront文档中的PHP示例):

    var so_canned = new SWFObject('http://location.domname.com/~jvngkhow/player.swf','mpl','640','360','9');
        so_canned.addParam('allowfullscreen','true');
        so_canned.addParam('allowscriptaccess','always');
        so_canned.addParam('wmode','opaque');
        so_canned.addVariable('file','video.mp4%3FExpires%3D1309979985%26Signature%3DMUNF7pw1689FhMeSN6JzQmWNVxcaIE9mk1x~KOudJky7anTuX0oAgL~1GW-ON6Zh5NFLBoocX3fUhmC9FusAHtJUzWyJVZLzYT9iLyoyfWMsm2ylCDBqpy5IynFbi8CUajd~CjYdxZBWpxTsPO3yIFNJI~R2AFpWx8qp3fs38Yw_%26Key-Pair-Id%3DAPKAIAZRKVIO4BQ');
        so_canned.addVariable('streamer','rtmp://s3nzpoyjpct.cloudfront.net/cfx/st');
        so_canned.write('canned');
    

    <强>摘要

    如你所见,不是很容易! boto v2将有助于大量设置发行版。我会发现是否有可能在其中获得一些URL生成代码以改进这个伟大的库!

答案 1 :(得分:3)

  

在Python中,为文件生成过期URL的最简单方法是什么。我已经安装了boto,但我不知道如何从流媒体发行版中获取文件。

您可以为资源生成过期的签名URL。 Boto3文档有一个nice example solution

import datetime

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from botocore.signers import CloudFrontSigner


def rsa_signer(message):
    with open('path/to/key.pem', 'rb') as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(), 
            password=None,
            backend=default_backend()
        )
    signer = private_key.signer(padding.PKCS1v15(), hashes.SHA1())
    signer.update(message)
    return signer.finalize()

key_id = 'AKIAIOSFODNN7EXAMPLE'
url = 'http://d2949o5mkkp72v.cloudfront.net/hello.txt'
expire_date = datetime.datetime(2017, 1, 1)

cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)

# Create a signed url that will be valid until the specfic expiry date
# provided using a canned policy.
signed_url = cloudfront_signer.generate_presigned_url(
    url, date_less_than=expire_date)
print(signed_url)