41

我创建了一个 S3 存储桶,上传了一个视频,在 CloudFront 中创建了一个流分发。使用静态 HTML 播放器对其进行了测试,并且可以正常工作。我通过帐户设置创建了一个密钥对。目前,我的桌面上有私钥文件。这就是我所在的地方。

我的目标是让我的 Django/Python 站点创建安全的 URL,并且人们无法访问视频,除非他们来自我的一个页面。问题是我对亚马逊的布局方式过敏,我越来越困惑。

我意识到这不会是 StackOverflow 上最好的问题,但我敢肯定,我不会是这里唯一一个对如何设置安全的 CloudFront/S3 情况不以为然的傻瓜。我非常感谢您的帮助,并且愿意(两天后)为最佳答案提供 500 分的赏金。

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

  • 在文档中(下一点有一个示例),周围有很多 XML 告诉我我需要POST到各个地方的东西。是否有用于执行此操作的在线控制台?还是我真的必须通过 cURL (等)强制执行此操作?

  • 如何为 CloudFront 创建源访问身份并将其绑定到我的分配?我已阅读此文档,但根据第一点,不知道如何处理它。我的密钥对如何适应这个?

  • 完成后,如何将 S3 存储桶限制为仅允许人们通过该身份下载内容?如果这是另一个 XML 工作,而不是在 Web UI 周围单击,请告诉我应该在哪里以及如何将其输入我的帐户。

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

  • 是否有任何应用程序或脚本可以解决设置此服装的困难?我使用 Ubuntu (Linux),但如果它是 Windows-only,我在虚拟机中安装 XP。我已经看过 CloudBerry S3 Explorer Pro - 但它与在线 UI 一样有意义。

4

2 回答 2

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“原始访问身份”(基本上是一个 AWS 帐户,允许 cloudfront 访问您的 s3 存储桶)
  3. 修改对象上的 ACL,以便只允许您的 Cloudfront 原始访问身份读取它们(这可以防止人们绕过 Cloudfront 并直接进入 s3)
  4. 创建具有基本 URL 和需要签名 URL 的云端分发
  5. 测试您是否可以从基本的云端分发下载对象,但不能从 s3 或签名的云端分发下载对象
  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 粘贴到新的“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 - 创建云端分发

请注意,在撰写本文时尚未正式发布的 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 - 应该可以正常工作

必须调整测试以与您的流播放器一起使用,但基本思想是只有基本的云端 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 中,您应该会得到如下所示的内容(来自 Amazon 的 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 生成代码来改进这个伟大的库!

于 2011-07-06T02:38:46.307 回答
3

在 Python 中,为文件生成过期 URL 的最简单方法是什么。我已经安装了 boto,但我看不到如何从流式分发中获取文件。

您可以为资源生成一个过期的签名 URL。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)
于 2016-03-15T12:11:22.457 回答