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

44 投票
2 回答
18348 浏览
提问于 2025-04-16 20:40

我创建了一个S3存储桶,上传了一个视频,并在CloudFront中创建了一个流媒体分发。用一个静态的HTML播放器测试了一下,结果是可以的。我在账户设置中创建了一个密钥对,目前私钥文件在我的桌面上。这就是我现在的情况。

我的目标是让我的Django/Python网站生成安全的链接,确保人们不能随便访问这些视频,只有从我的页面过来的用户才能访问。问题是,我对亚马逊的设置方式感到很困惑,越来越搞不清楚了。

我知道这个问题可能不是StackOverflow上最好的问题,但我相信我不是唯一一个搞不懂如何设置安全的CloudFront/S3环境的人。我非常希望能得到帮助,并且愿意在两天后给最佳答案提供500分的奖励。

我有几个问题,一旦解答了,就能帮助我理解如何实现我的目标:

  • 在文档中(下一个点有个例子),有很多XML文件告诉我需要把东西POST到不同的地方。有没有在线控制台可以用来做这些?还是说我真的得用cURL等工具来强行上传?

  • 我该如何为CloudFront创建一个源访问身份,并将其绑定到我的分发上?我看过这份文档,但根据第一个问题,我不知道该怎么做。我的密钥对在这里有什么用?

  • 完成这些后,我该如何限制S3存储桶,只允许通过那个身份下载文件?如果这又是个XML的事情,而不是在网页界面上点击就能解决的,请告诉我该在哪里以及如何将其添加到我的账户中。

  • 在Python中,生成一个过期的文件链接最简单的方法是什么?我已经安装了boto,但我不知道如何从流媒体分发中获取文件。

  • 有没有什么应用程序或脚本可以简化这个设置过程?我使用的是Ubuntu(Linux),但如果是Windows专用的话,我在虚拟机里有XP。我已经看过CloudBerry S3 Explorer Pro,但它和在线界面一样让我感到困惑。

2 个回答

3

在Python中,生成一个会过期的文件链接最简单的方法是什么?我已经安装了boto,但我不知道如何从流媒体分发中获取文件。

你可以为这个资源生成一个会过期的签名链接。Boto3的文档中有一个很好的示例解决方案

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

你说得对,设置这个需要很多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的“源访问身份”(基本上是一个AWS账户,用于允许Cloudfront访问你的S3存储桶)
  3. 修改你的对象的访问控制列表(ACL),确保只有你的Cloudfront源访问身份可以读取它们(这样可以防止人们绕过Cloudfront直接访问S3)
  4. 创建一个Cloudfront分发,包含基本的URL和一个需要签名的URL
  5. 测试你是否可以从基本的Cloudfront分发下载对象,但不能从S3或签名的Cloudfront分发下载
  6. 创建一个用于签名URL的密钥对
  7. 使用Python生成一些URL
  8. 测试签名的URL是否有效

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的“源访问身份”

目前,这一步只能通过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对象的权限。我们可以通过AWS控制台轻松做到这一点,打开对象的(而不是存储桶的!)权限标签,点击“添加更多权限”按钮,然后把上面得到的很长的S3CanonicalUserId粘贴到新权限的“受让人”字段中。确保给新权限赋予“打开/下载”的权限。

你也可以使用以下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 - 创建Cloudfront分发

请注意,直到boto版本2.0,定制源和私有分发才得到完全支持,而在写这篇文章时,2.0版本还没有正式发布。下面的代码从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 - 应该正常工作

这些测试需要根据你的流媒体播放器进行调整,但基本的想法是,只有基本的Cloudfront URL应该可以工作。

6 - 为CloudFront创建密钥对

我认为唯一的方法是通过亚马逊的网站。进入你的AWS“账户”页面,点击“安全凭证”链接。点击“密钥对”标签,然后点击“创建新的密钥对”。这将为你生成一个新的密钥对,并自动下载一个私钥文件(pk-xxxxxxxxx.pem)。请妥善保管这个密钥文件,并确保它是私密的。同时记下亚马逊的“密钥对ID”,因为我们在下一步需要用到它。

7 - 在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(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

希望你现在应该有一个有效的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生成的代码,以改善这个很棒的库!

撰写回答