从iPhone应用程序上传照片和S3的建筑和设计问题

49

我希望允许iPhone应用程序的用户上传照片并使用Amazon S3。有两种方法可以实现:

  1. 从iPhone上传到我的服务器,然后代理到Amazon S3。
  2. 直接从iPhone上传到S3

对于选项1,安全性很简单。我永远不必告诉iPhone我的S3密码。缺点是所有上传都通过我们的服务器代理,这有点违背了使用S3的初衷。

对于选项2,理论上更好,但问题在于如何使iPhone(或任何其他平台上的移动应用程序)在不提供我的密码的情况下写入我的S3存储桶?此外,我不确定这是否是一个好的设计,因为流程是:iPhone上传到S3,获取URL,然后告诉服务器URL是什么,以便将来引用。但是,由于通信分为两个步骤(iPhone->S3 vs iPhone->My-Server),所以它作为非原子操作变得脆弱。

我找到了一些旧的信息,提到使用基于浏览器的POST上传,但不确定这是否仍然是推荐的方法。我希望有一个更好的解决方案,可以只使用REST API而不是依赖于POST。我还看到了AWS iOS Beta SDK,但他们的文档没有多大帮助,我找到了一篇Amazon文章同样没有帮助,因为它警告你不要做什么,但没有告诉你替代方法:
移动AWS SDK会对发送到亚马逊网络服务(AWS)的API请求进行签名,以验证发起请求的AWS账号的身份。否则,恶意开发者可以轻松地向另一个开发者的基础架构发出请求。这些请求使用AWS提供的AWS Access Key ID和Secret Access Key进行签名。 Secret Access Key类似于密码,并且保密非常重要。
提示:您可以在AWS网站上查看所有AWS安全凭证,包括Access Key ID和Secret Access Key,网址是http://aws.amazon.com/security-credentials
将凭据嵌入源代码对软件(包括移动应用程序)来说是有问题的,因为恶意用户可以反编译软件或查看源代码以检索Secret Access Key。
有人有关于最佳架构设计和流程的建议吗?
更新:我越深入研究,似乎有一堆人推荐使用HTTP POST方法,并使用json策略文件,具体请参见http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html?UsingHTTPPOST.html

有了这个,流程大致如下:(1)iPhone向我的服务器发出请求,请求策略文件(2)服务器生成JSON策略文件并返回给客户端(3)iPhone将照片和JSON策略进行HTTP POST到S3。我讨厌我正在以一种明显笨拙的方式使用HTTP POST,但似乎更好,因为它消除了我的服务器完全存储照片的需要。


很遗憾我无法提供答案:我看到你所看到的每条路线的优缺点。不过,我可以帮助你使用S3 REST API,ASIHTTPRequest具有出色的S3支持:http://allseeing-i.com/ASIHTTPRequest/S3 - Matthew Frederick
Matthew,感谢你提供关于ASIHTTPRequest的提示。相比AWS提供的iOS Beta SDK,它有什么优势吗? - TMC
我不知道有什么原因。我只是在各种数据传输情况下使用ASI库取得了良好的成功,所以没有尝试其他东西。 - Matthew Frederick
Joe,我觉得我不能使用iOS钥匙串,因为它是用来安全存储敏感数据的,以便其他用户或恶意应用无法提取。然而,用户自己可以从中提取项目。请参考:http://blog.crackpassword.com/2010/08/peeking-inside-keychain-secrets/ - TMC
1
更新:看起来有两种方法可以做到这一点。首先,它可以通过我的服务器代理,但这样做的缺点是将我的服务器置于每个交易的中间。优点是在多个通信环节中减少了错误点。第二种方法是使用Adrian Petrescu指出的AWS的“预签名URL”。 - TMC
我在下面添加了一个答案,允许iPhone使用REST API的PUT方法,并委托服务器生成需要密钥的Authorization头部分。这样,您就不会冒着将访问密钥暴露给越狱iPhone用户的风险,同时也不会将上传文件的负担放在服务器上。 - shadowmatter
5个回答

10

我之前在 AWS 论坛上讨论过这个问题。正如我在那里所说的,从移动设备访问 AWS 的正确解决方案是使用AWS 身份和访问管理 (IAM)服务为每个用户生成临时、有限特权的访问密钥。这项服务很棒,但目前仍处于测试阶段,并未包含在移动 SDK 中。我猜一旦这个功能发布正式版,你会立即在移动 SDK 中看到它。

在此之前,可以为您的用户生成预签名 URL,或者像其他人建议的那样通过自己的服务器代理。预签名 URL 可以让您的用户在没有实际凭据(它们被哈希成签名)的情况下暂时地获取或放置 S3 桶中的对象。您可以在这里了解详细信息

编辑:我已经使用 IAM 的预览版实现了这个问题的适当解决方案。它在GitHub 上可用,您可以在这里阅读有关它的信息


感谢您提供有关AWS IAM的提示。很遗憾它仍处于测试阶段。您能详细说明一下您提到的“预签名URL”解决方案吗?这只是使用带有JSON策略文档的HTTP POST吗? - TMC
嘿TMC,我在我的回答中添加了有关预签名URL的更多细节(和两个链接)。希望这可以帮到你 :) - Adrian Petrescu
在我的快速阅读中,客户端需要从我的服务器请求预签名URL,因为它基于AWS秘钥。然后它将使用该预签名URL来进行文件上传。因此,本质上,这与之前提到的HTTP POST方法没有区别,对吗? - TMC
访问密钥被他人知晓是可以的。事实上,它包含在每个请求的URL中。而你必须对秘密密钥保持警惕。 - Adrian Petrescu
啊,你说得对。我把“AWS访问密钥”和“AWS 秘密访问密钥”搞混了。谢谢你指出来。现在我需要发布一个关于.NET代码签名URL的后续问题 :) - TMC
显示剩余2条评论

5
你可以使用 REST API 直接从 iPhone 上传到 S3,并让服务器负责生成需要密钥的 Authorization 标头值的部分。这样,您就不会冒着将访问密钥暴露给越狱 iPhone 用户的风险,同时也不会将文件上传的负担放在服务器上。有关要进行的请求的详细信息,请参见“签署和认证 REST 请求”的“示例对象 PUT”"Example Object PUT"。我强烈建议在继续之前阅读该文档。
以下 Python 代码生成从您的 S3 秘密访问密钥派生的 Authorization 标头值的部分。您应该将自己的秘密访问密钥和虚拟主机形式下的存储桶名称分别替换为下面的“_S3_SECRET”和“_S3_BUCKET_NAME”。
import base64
from datetime import datetime
import hmac
import sha

# Replace these values.
_S3_SECRET = "my-s3-secret"
_S3_BUCKET_NAME = "my-bucket-name"

def get_upload_header_values(content_type, filename): 
  now = datetime.utcnow()
  date_string = now.strftime("%a, %d %b %Y %H:%M:%S +0000")
  full_pathname = '/%s/%s' % (_S3_BUCKET_NAME, filename)
  string_to_sign = "PUT\n\n%s\n%s\n%s" % (
      content_type, date_string, full_pathname)
  h = hmac.new(_S3_SECRET, string_to_sign, sha)
  auth_string = base64.encodestring(h.digest()).strip()
  return (date_string, auth_string)

使用文件名foo.txt和内容类型text/plain调用此函数会产生以下结果:
>>> get_upload_header_values('text/plain', 'foo.txt')
('Wed, 06 Feb 2013 00:57:45 +0000', 'EUSj3g70aEsEqSyPT/GojZmY8eI=')

请注意,如果您运行此代码,则返回的时间将不同,因此编码的HMAC摘要也将不同。
现在,iPhone客户端只需使用返回的日期和HMAC摘要向S3发出PUT请求。 假设
  • 服务器在名为serverJson的某个JSON对象中返回了上面的date_stringauth_string
  • 您的S3访问密钥(而不是仅在服务器上的秘密)被命名为kS3AccessKey
  • 您的S3存储桶名称(设置为上面的my-bucket-name)被命名为kS3BucketName
  • 文件内容被编组到名为dataNSData对象中
  • 发送到服务器的文件名是一个名为filename的字符串
  • 发送到服务器的内容类型是一个名为contentType的字符串
然后,您可以执行以下操作创建NSURLRequest:
NSString *serverDate = [serverJson objectForKey:@"date"]
NSString *serverHmacDigest = [serverJson objectForKey:@"hmacDigest"]

// Create the headers.
NSMutableDictionary *headers = [[NSMutableDictionary alloc] init];
[headers setObject:contentType forKey:@"Content-Type"];
NSString *host = [NSString stringWithFormat:@"%@.s3.amazonaws.com", kS3BucketName]
[headers setObject:host forKey:@"Host"];
[headers setObject:serverDate forKey:@"Date"];
NSString *authorization = [NSString stringWithFormat:@"AWS %@:%@", kS3AccessKey, serverHmacDigest];
[headers setObject:authorization forKey:@"Authorization"];

// Create the request.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:data];
[request setHTTPMethod:@"PUT"];
NSString *postUrl = [NSString stringWithFormat:@"http://%@.s3.amazonaws.com/%@", kS3BucketName, filename];
[request setURL:[NSURL URLWithString:postUrl]];

接下来,您可以发出请求。如果您正在使用优秀的AFNetworking库,那么您可以使用XMLDocumentRequestOperationWithRequest:success:failure:request包装在一个AFXMLRequestOperation对象中,然后调用其start方法。完成后不要忘记释放headersrequest

请注意,客户端从服务器获取了Date头的值。这是因为,正如亚马逊在"时间戳要求"下所描述的那样:

“对于已认证的请求,必须使用有效的时间戳(使用HTTP日期头或x-amz-date替代方案)。此外,包含在已认证请求中的客户端时间戳必须在收到请求时与Amazon S3系统时间相差不超过15分钟。否则,请求将失败并显示RequestTimeTooSkewed错误状态代码。”
“因此,不要依赖于客户端时间是否正确以使请求成功,而是依赖于服务器,服务器应该使用NTP(和类似ntpd的守护程序)。 ”

2
Shadowmatter是对的。直接上传到S3是更好的选择。他的代码很好用。我已经将我的fork放在gist中(https://gist.github.com/coudron/5258947),其中包含一些示例Python代码和Objective-C代码。这也是一个绕过Heroku 30秒请求超时问题的好方法。 - coudron
我正在尝试实现上述内容,但一直收到405错误 - 方法不允许。我需要修改存储桶的策略以启用上述内容吗? - maethorr
@maethorr 虽然我已经不在这个项目上了,但我认为一个不好的策略应该会生成401或403错误代码。我猜你可能是意外地使用了_POST_而不是_PUT_? - shadowmatter
@shadowmatter 发现了问题。我将其放在了错误的端点上(网站端点 <bucketname>.s3-website-ap-southeast-2.amazonaws.com 而不是只有 <bucketname>.s3.amazonaws.com)。感谢您的回复。 - maethorr

5

上传到您的服务器,然后再上传到S3。从架构角度来看,您将希望从您的服务器执行此操作。在数据传输期间可能会出现许多问题,您可以在服务器上更好地处理这些问题,如果您想存储有关发送到S3的图像的任何数据,则可能需要进行服务器端调用。

此外,您的Secret Access Key存储在更安全的环境中。就是您自己的服务器。

如果您担心可扩展性,并且将执行大量S3传输,则应考虑在EC2实例上托管您的服务器。在两者之间传输数据是免费的(前提是您将数据存储在以下数据中心中)。

在同一区域内Amazon EC2和Amazon S3之间或在Amazon EC2北弗吉尼亚地区和Amazon S3 US标准区域之间传输的数据不收取数据传输费用。" Amazon Simple Storage Service (Amazon S3)

这里有很多关于使用EC2的利弊的帖子Amazon - EC2 cost?(例如)。


1
EC2<->S3信息的重要性得到了提高。 - ceejayoz
我们在Azure上运行,因此转移到EC2不是一个选项。我在原帖中提到,我的服务器作为代理似乎不是唯一的方法,因为亚马逊支持通过HTTP POST和JSON策略文件上传。如果有一种方法可以避免我的服务器成为中间人,那么这是显而易见的理想方法。 - TMC
我更喜欢直接上传图像,但仍然在我的网站服务器数据库上存储引用和元数据。 - David van Dugteren
从服务器上传不具有良好的可扩展性。 - Bruno Lemos

0
我感到困惑。为什么亚马逊会推出 iOS SDK 来上传数据至 S3,然后告诉我们不要使用它(在软件中嵌入凭证,包括移动应用程序,都会存在问题,因为恶意用户可以反编译软件或查看源代码来检索 Secret Access Key)???

Joe,亚马逊提供了一个iPhone SDK,这解释了Dat的困惑。 - TMC

0

他们可能提供了SDK,以便应用程序可以允许对个人S3帐户进行身份验证?例如,一个应用程序可以让用户将文件存储在他们自己的(用户的)存储桶中,而不是提供商的存储桶。我认为将密钥与应用程序合并并分发会存在安全漏洞。一旦密钥被泄露,任何人都可以(误)使用它们(当您将其公开时,它永远不安全)。另一方面,将功能保留到服务器上将使您的密钥对用户透明,不是吗?


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