开始使用 Python 进行安全 AWS CloudFront 流式传输
Posted
技术标签:
【中文标题】开始使用 Python 进行安全 AWS CloudFront 流式传输【英文标题】:Getting started with secure AWS CloudFront streaming with Python 【发布时间】:2011-09-26 20:18:42 【问题描述】:我创建了一个 S3 存储桶,上传了一个视频,在 CloudFront 中创建了一个流式分配。使用静态 html 播放器对其进行了测试,并且可以正常工作。我通过帐户设置创建了一个密钥对。目前,我的桌面上有私钥文件。这就是我所在的地方。
我的目标是让我的 Django/Python 网站创建安全 URL,并且人们无法访问视频,除非他们来自我的某个页面。问题是我对亚马逊的布局方式过敏,而且我越来越困惑。
我意识到这不会是 *** 上最好的问题,但我敢肯定,我不会是这里唯一一个对如何设置安全 CloudFront 不以为然的傻瓜/ S3情况。我非常感谢您的帮助,并愿意(两天后)为最佳答案提供 500 分的赏金。
我有几个问题,一旦得到解答,就应该成为如何完成我所追求的目标的一种解释:
在文档中(下一点中有一个示例),周围有很多 XML 告诉我我需要 POST
到各个地方的东西。是否有用于执行此操作的在线控制台?还是我真的必须通过 cURL(等)强制执行此操作?
如何为 CloudFront 创建源访问身份并将其绑定到我的分配?我读过this document 但是,根据第一点,不知道该怎么办。我的密钥对如何适应这个?
完成后,如何将 S3 存储桶限制为仅允许人们通过该身份下载内容?如果这是另一个 XML 工作,而不是在 Web UI 周围单击,请告诉我应该在哪里以及如何将其输入我的帐户。
在 Python 中,为文件生成过期 URL 的最简单方法是什么。我安装了boto
,但我看不到如何从流分发中获取文件。
是否有任何应用程序或脚本可以解决设置此服装的困难?我使用 Ubuntu (Linux),但如果它是 Windows-only,我在虚拟机中安装 XP。我已经看过 CloudBerry S3 Explorer Pro - 但它与在线 UI 一样有意义。
【问题讨论】:
您在问题中提到您使用“静态 HTML 播放器”这可能吗!我认为播放 rtmp 的唯一方法是使用基于 Flash 的播放器。如果可能的话,请回复教程。谢谢 在这个问题以及接受的答案之后过了一段时间,几乎没有什么改变。 Boto3 被引入。因此,我在下面的回答中详细介绍了从新 Boto 文档签署 URL 的过程。当然,我的回答也会老化 【参考方案1】:您说得对,设置此设置需要大量 API 工作。我希望他们尽快在 AWS 控制台中获得它!
更新:我已将此代码提交给 boto - 从 boto v2.1(2011-10-27 发布)开始,这变得容易多了。对于 boto http://www.secretmike.com/2011/10/aws-cloudfront-secure-streaming.html 一旦 boto v2.1 被更多发行版打包,我将在此处更新答案。
要完成您想要的,您需要执行以下步骤,我将在下面详细说明:
-
创建您的 s3 存储桶并上传一些对象(您已经这样做了)
创建 Cloudfront“原始访问身份”(基本上是一个 AWS 帐户,以允许 Cloudfront 访问您的 s3 存储桶)
修改对象上的 ACL,以便只允许您的 Cloudfront 原始访问身份读取它们(这可以防止人们绕过 Cloudfront 并直接进入 s3)
创建具有基本 URL 和需要签名 URL 的云端分发版
测试您是否可以从基本的云端分发版下载对象,但不能从 s3 或签名的云端分发版下载对象
创建用于签名 URL 的密钥对
使用 Python 生成一些 URL
测试签名 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 粘贴到新的“Grantee”字段中。确保您授予新权限“打开/下载”权限。
您也可以使用以下 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 - 创建云端分发
请注意,boto 直到 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 - 应该拒绝访问 signed_distribution.cloudfront.net/video.mp4 - 应该提供 MissingKey(因为 URL 未签名) basic_distribution.cloudfront.net/video.mp4 - 应该可以正常工作必须调整测试以与您的流播放器一起使用,但基本思想是只有基本的云端 URL 才能工作。
6 - 为 CloudFront 创建密钥对
我认为唯一的方法是通过亚马逊的网站。进入您的 AWS“帐户”页面,然后单击“安全凭证”链接。单击“密钥对”选项卡,然后单击“创建新密钥对”。这将为您生成一个新的密钥对并自动下载一个私钥文件(pk-xxxxxxxxx.pem)。保持密钥文件的安全和私密。还要记下亚马逊的“密钥对 ID”,因为我们将在下一步中使用它。
7 - 在 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(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 生成代码以改进这个伟大的库!
【讨论】:
这是一个很好的开始。我几乎等不及下半场了! 重申一下,现在你已经完成了这篇文章,这真的很棒。我对 Web UI 的可悲程度以及他们希望用户为完成简单的广告功能所做的繁重工作感到震惊。我明天试试这个,然后回复你。 好吧。万岁!权限设置和测试,流式 URL 生成,播放器正常工作,Oli 很高兴。对于某些播放器——在我的例子中是 JW 播放器——我要补充一点——你必须从文件名中删除文件的扩展名,这样video.flv%3...
就变成了video%3...
。不知道它为什么需要它或者为什么它甚至可以工作......但是你去了。赏金赚得盆满钵满。
很好,我见过的最全面的 SO 答案之一。谢谢
S3 支持基于 HTTP Referer 的限制,但 cloudfront 目前仅支持时间和源 IP 地址。对于热链接,按照此处所述生成过期 URL 应该可以工作。任何直接链接到您的内容的内容都将在几分钟后停止工作。对于爬虫,我推荐 robots.txt【参考方案2】:
在 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)
【讨论】:
我在private_key = serialization.load_pem_private_key( key_file.read(), password=None, backend=default_backend() )
行中遇到错误,说UnsupportedAlgorithm: This backend does not support this key serialization.
知道为什么会这样。我正在按照示例加载 pem 文件。看看这个问题link以上是关于开始使用 Python 进行安全 AWS CloudFront 流式传输的主要内容,如果未能解决你的问题,请参考以下文章
用于管理 AWS 无服务器基础设施的 Terraform 或 cloudformation [关闭]