iphone - NSURLSession 和亚马逊 S3 上传

标签 iphone ios cocoa-touch amazon-s3 nsurlsession

我有一个正在将图像上传到 amazon S3 的应用程序。我一直在尝试将其从使用 NSURLConnection 切换到 NSURLSession,以便在应用程序处于后台时可以继续上传!我似乎遇到了一些问题。 NSURLRequest 已创建并传递给 NSURLSession,但亚马逊发回 403 - 禁止响应,如果我将相同的请求传递给 NSURLConnection,它会完美上传文件。

这是创建响应的代码:

NSString *requestURLString = [NSString stringWithFormat:@"http://%@.%@/%@/%@", BUCKET_NAME, AWS_HOST, DIRECTORY_NAME, filename];
NSURL *requestURL = [NSURL URLWithString:requestURLString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL
                                                       cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
                                                   timeoutInterval:60.0];
// Configure request
[request setHTTPMethod:@"PUT"];
[request setValue:[NSString stringWithFormat:@"%@.%@", BUCKET_NAME, AWS_HOST] forHTTPHeaderField:@"Host"];
[request setValue:[self formattedDateString] forHTTPHeaderField:@"Date"];
[request setValue:@"public-read" forHTTPHeaderField:@"x-amz-acl"];
[request setHTTPBody:imageData];

然后这是对响应的签名(我认为这来自另一个 SO 答案):

NSString *contentMd5  = [request valueForHTTPHeaderField:@"Content-MD5"];
NSString *contentType = [request valueForHTTPHeaderField:@"Content-Type"];
NSString *timestamp   = [request valueForHTTPHeaderField:@"Date"];

if (nil == contentMd5)  contentMd5  = @"";
if (nil == contentType) contentType = @"";

NSMutableString *canonicalizedAmzHeaders = [NSMutableString string];

NSArray *sortedHeaders = [[[request allHTTPHeaderFields] allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];

for (id key in sortedHeaders)
{
    NSString *keyName = [(NSString *)key lowercaseString];
    if ([keyName hasPrefix:@"x-amz-"]){
        [canonicalizedAmzHeaders appendFormat:@"%@:%@\n", keyName, [request valueForHTTPHeaderField:(NSString *)key]];
    }
}

NSString *bucket = @"";
NSString *path   = request.URL.path;
NSString *query  = request.URL.query;

NSString *host  = [request valueForHTTPHeaderField:@"Host"];

if (![host isEqualToString:@"s3.amazonaws.com"]) {
    bucket = [host substringToIndex:[host rangeOfString:@".s3.amazonaws.com"].location];
}

NSString* canonicalizedResource;

if (nil == path || path.length < 1) {
    if ( nil == bucket || bucket.length < 1 ) {
        canonicalizedResource = @"/";
    }
    else {
        canonicalizedResource = [NSString stringWithFormat:@"/%@/", bucket];
    }
}
else {
    canonicalizedResource = [NSString stringWithFormat:@"/%@%@", bucket, path];
}

if (query != nil && [query length] > 0) {
    canonicalizedResource = [canonicalizedResource stringByAppendingFormat:@"?%@", query];
}

NSString* stringToSign = [NSString stringWithFormat:@"%@\n%@\n%@\n%@\n%@%@", [request HTTPMethod], contentMd5, contentType, timestamp, canonicalizedAmzHeaders, canonicalizedResource];

NSString *signature = [self signatureForString:stringToSign];

[request setValue:[NSString stringWithFormat:@"AWS %@:%@", self.S3AccessKey, signature] forHTTPHeaderField:@"Authorization"];

然后如果我使用这行代码:

[NSURLConnection connectionWithRequest:request delegate:self];

它可以工作并上传文件,但如果我使用:

NSURLSessionUploadTask *task = [self.session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:filePath]];
[task resume];

我得到了禁止的错误......!?

有没有人尝试用这个上传到 S3 并遇到类似的问题?我想知道这是否与 session 暂停和恢复上传的方式有关,或者它对请求做了一些有趣的事情..?

一个可能的解决方案是将文件上传到我控制的临时服务器,并在完成后将其转发到 S3...但这显然不是一个理想的解决方案!

非常感谢任何帮助!!

谢谢!

最佳答案

我根据 Zeev Vax 的回答让它工作。我想就我遇到的问题提供一些见解并提供一些小的改进。

构建一个普通的 PutRequest,例如

S3PutObjectRequest* putRequest = [[S3PutObjectRequest alloc] initWithKey:keyName inBucket:bucketName];

putRequest.credentials = credentials;
putRequest.filename = theFilePath;

现在我们需要做一些 S3Client 通常为我们做的工作

// set the endpoint, so it is not null
putRequest.endpoint = s3Client.endpoint;

// if you are using session based authentication, otherwise leave it out
putRequest.securityToken = messageTokenDTO.securityToken;

// sign the request (also computes md5 checksums etc.)
NSMutableURLRequest *request = [s3Client signS3Request:putRequest];

现在将所有这些复制到一个新请求中。亚马逊使用他们自己的 NSUrlRequest 类,这会导致异常

NSMutableURLRequest* request2 = [[NSMutableURLRequest alloc]initWithURL:request.URL];
[request2 setHTTPMethod:request.HTTPMethod];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];

现在我们可以开始实际传输了

NSURLSession* backgroundSession = [self backgroundSession];
_uploadTask = [backgroundSession uploadTaskWithRequest:request2 fromFile:[NSURL fileURLWithPath:theFilePath]];
[_uploadTask resume];

这是创建后台 session 的代码:

- (NSURLSession *)backgroundSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.example.my.unique.id"];
        session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    });

    return session;
}

我花了一段时间才弄清楚 session /任务委托(delegate)需要处理身份验证挑战(我们实际上是对 s3 的身份验证)。所以只需实现

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
    NSLog(@"session did receive challenge");
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

关于iphone - NSURLSession 和亚马逊 S3 上传,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19478904/

相关文章:

iphone - CoreData 版本控制和阻止轻量级迁移

iphone - 背景图像在 iPhone4 上无法正确呈现

php - 在 iPhone 应用程序中使用 PHP 编码 JSON

iphone - 在应用程序购买测试帐户不能在 IOS 中工作?

php - 使用 AFNetworking 上传 POST jpeg

ios - NSFetchedResultsController 加载整个数据和糟糕的性能

android - flutter 图表 : display value above each point

iphone - 如果没有声明 nsautoreleasepool,autorelease 调用是否会崩溃?

iphone - XCode:当用户点击 UITextbox 时显示 UIDatePicker

ios - 带有导航栏大标题的 UITableView 奇怪的滚动行为,滚动到顶部时顶部的弹跳效果自动切断/生涩