通过 Obj-c 查询 REST API 时出现 Azure DocumentDB 间歇性 401 错误
Azure DocumentDB Intermittent 401 error when querying REST API via Obj-c
我负责使用 REST API 方案实施基于 objective-c 的 iOS Azure DocumentDB 系统查询。利用在 github 上找到的代码,特别是 https://github.com/Azure/azure-storage-ios 我能够生成一个适当验证的请求和 returns 适当的数据....有时。
问题:我间歇性地从服务器收到 401(身份验证失败)错误响应。通过 Node.js 发出相同的请求不会遇到此行为,因此我认为这是我的 objective-c 实现的问题。
- (NSMutableURLRequest *) RequestWithQuery:(NSString*)query Parameters:(NSArray*)parameters {
NSError* error;
NSDictionary* dictionaryOfBodyContents = @{@"query":query,
@"parameters":parameters};
NSData* body = [NSJSONSerialization dataWithJSONObject:dictionaryOfBodyContents
options:NSJSONWritingPrettyPrinted
error:&error];
if(error != nil) {
NSLog(@"AzureRequestWithQueryParameters error generating the body: %@",error);
return nil;
}
char buffer[30];
struct tm * timeptr;
time_t time = (time_t) [[NSDate date] timeIntervalSince1970];
timeptr = gmtime(&time);
if (!strftime_l(buffer, 30, [@"%a, %d %b %Y %T GMT" UTF8String], timeptr, NULL))
{
NSException* myException = [NSException
exceptionWithName:@"Error in date/time format"
reason:@"Unknown"
userInfo:nil];
@throw myException;
}
NSString* date = [NSString stringWithUTF8String:buffer];
// generate auth token
NSString* authorizationToken = [self AuthorizationTokenForTableQueryWithDate:date];
// generate header contents
NSDictionary* dictionaryOfHeaderContents = @{@"authorization":authorizationToken,
@"connection":AZURE_CONNECTION_HEADER_CONNECTION,
@"content-type":AZURE_CONNECTION_HEADER_CONTENTTYPE,
@"content-length":[NSString stringWithFormat:@"%lu",(unsigned long)[body length]],
@"x-ms-version":AZURE_CONNECTION_APIVERSION,
@"x-ms-documentdb-isquery":@"true",
@"x-ms-date":date.lowercaseString,
@"cache-control":@"no-cache",
@"user-agent":AZURE_CONNECTION_HEADER_USERAGENT,
@"accept":@"application/json"};
// generate url contents
NSString* urlString = [NSString stringWithFormat:@"https://%@:%@/%@", AZURE_URL_HOST, AZURE_URL_PORT, AZURE_URL_DOCUMENTS];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:AZURE_CONNECTION_METHOD];
[request setAllHTTPHeaderFields:dictionaryOfHeaderContents];
[request setHTTPBody:body];
return request;
}
- (NSString*) AuthorizationTokenForTableQueryWithDate:(NSString*)date {
//
// Based on https://msdn.microsoft.com/en-us/library/azure/dd179428.aspx under "Table Service (Shared Key Authentication)"
//
// generating a authentication token is a Hash-based Message Authentication Code (HMAC) constructed from the request
// and computed by using the SHA256 algorithm, and then encoded by using Base64 encoding.
//
// StringToSign = VERB + "\n" +
// Content-MD5 + "\n" +
// Content-Type + "\n" +
// Date + "\n" +
// CanonicalizedHeaders +
// CanonicalizedResource;
//
NSString* StringToSign = [NSString stringWithFormat:@"%@\n%@\n%@\n%@\n\n",
AZURE_CONNECTION_METHOD.lowercaseString?:@"",
AZURE_RESOURCE_TYPE.lowercaseString?:@"",
AZURE_URL_COLLECTIONS.lowercaseString?:@"",
date.lowercaseString?:@""];
// Generate Key/Message pair
NSData* keyData = [[NSData alloc] initWithBase64EncodedString:AZURE_AUTH_KEY options:NSDataBase64DecodingIgnoreUnknownCharacters];
NSData* messageData = [StringToSign dataUsingEncoding:NSUTF8StringEncoding];
// Encrypt your Key/Message using HMAC SHA256
NSMutableData* HMACData = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, keyData.bytes, keyData.length, messageData.bytes, messageData.length, HMACData.mutableBytes);
// Take your encrypted data, and generate a token that Azure likes.
NSString* signature = [HMACData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSString* unencodedToken = [NSString stringWithFormat:@"type=master&ver=1.0&sig=%@",signature];
NSString* authorizationToken = [unencodedToken stringByReplacingOccurrencesOfString:@"&" withString:@"%26"];
authorizationToken = [authorizationToken stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
return authorizationToken;
}
如果有人遇到类似的间歇性 401 并且能够解决任何帮助,我们将不胜感激。或针对上述代码的调试步骤的建议,我曾尝试将时间戳减少几秒钟,类似的间歇性故障。
尽管在失败时简单地重试几次并减少秒数会导致在 1-2 次重试后得到 200 次响应,但我认为这无论如何都不是理想的解决方案。
感谢您的宝贵时间。
更新:失败的原因请看下面Andrew Liu的解释。我已将他的回复标记为答案,下面是更新后的代码片段。
NSString* unencodedToken = [NSString stringWithFormat:@"type=master&ver=1.0&sig=%@",signature];
// NSString* authorizationToken = [unencodedToken stringByReplacingOccurrencesOfString:@"&" withString:@"%26"];
// authorizationToken = [authorizationToken stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
NSString* authorizationToken = [unencodedToken stringByAddingPercentEncodingWithAllowedCharacters:[[NSCharacterSet characterSetWithCharactersInString:@"&+="] invertedSet]];
return authorizationToken;
我以前见过这个问题,通常它与协议的最后 2 个步骤有关,即 base64 编码很糟糕,或者 URI 编码。调试此问题的一种方法是打印您发送的身份验证令牌,以防失败,并查看是否有任何可能未正确传输的奇怪字符。你可以post这里的越野车令牌,我可以看看。
401(认证失败)通常表示认证令牌有问题。
请务必注意,身份验证令牌是 Base64 编码的字符串 - 这意味着它可以包含 +
字符。
数据库服务器期望身份验证令牌中的 +
个字符被 url 编码(%2B
)...一些但不是所有的 HTTP 客户端将自动编码 HTTP headers 给你。
我怀疑 url-encoding 或将以下变量的 +
转换为 %2B
将解决间歇性 401 问题:
NSString* signature = [HMACData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
我负责使用 REST API 方案实施基于 objective-c 的 iOS Azure DocumentDB 系统查询。利用在 github 上找到的代码,特别是 https://github.com/Azure/azure-storage-ios 我能够生成一个适当验证的请求和 returns 适当的数据....有时。
问题:我间歇性地从服务器收到 401(身份验证失败)错误响应。通过 Node.js 发出相同的请求不会遇到此行为,因此我认为这是我的 objective-c 实现的问题。
- (NSMutableURLRequest *) RequestWithQuery:(NSString*)query Parameters:(NSArray*)parameters {
NSError* error;
NSDictionary* dictionaryOfBodyContents = @{@"query":query,
@"parameters":parameters};
NSData* body = [NSJSONSerialization dataWithJSONObject:dictionaryOfBodyContents
options:NSJSONWritingPrettyPrinted
error:&error];
if(error != nil) {
NSLog(@"AzureRequestWithQueryParameters error generating the body: %@",error);
return nil;
}
char buffer[30];
struct tm * timeptr;
time_t time = (time_t) [[NSDate date] timeIntervalSince1970];
timeptr = gmtime(&time);
if (!strftime_l(buffer, 30, [@"%a, %d %b %Y %T GMT" UTF8String], timeptr, NULL))
{
NSException* myException = [NSException
exceptionWithName:@"Error in date/time format"
reason:@"Unknown"
userInfo:nil];
@throw myException;
}
NSString* date = [NSString stringWithUTF8String:buffer];
// generate auth token
NSString* authorizationToken = [self AuthorizationTokenForTableQueryWithDate:date];
// generate header contents
NSDictionary* dictionaryOfHeaderContents = @{@"authorization":authorizationToken,
@"connection":AZURE_CONNECTION_HEADER_CONNECTION,
@"content-type":AZURE_CONNECTION_HEADER_CONTENTTYPE,
@"content-length":[NSString stringWithFormat:@"%lu",(unsigned long)[body length]],
@"x-ms-version":AZURE_CONNECTION_APIVERSION,
@"x-ms-documentdb-isquery":@"true",
@"x-ms-date":date.lowercaseString,
@"cache-control":@"no-cache",
@"user-agent":AZURE_CONNECTION_HEADER_USERAGENT,
@"accept":@"application/json"};
// generate url contents
NSString* urlString = [NSString stringWithFormat:@"https://%@:%@/%@", AZURE_URL_HOST, AZURE_URL_PORT, AZURE_URL_DOCUMENTS];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:AZURE_CONNECTION_METHOD];
[request setAllHTTPHeaderFields:dictionaryOfHeaderContents];
[request setHTTPBody:body];
return request;
}
- (NSString*) AuthorizationTokenForTableQueryWithDate:(NSString*)date {
//
// Based on https://msdn.microsoft.com/en-us/library/azure/dd179428.aspx under "Table Service (Shared Key Authentication)"
//
// generating a authentication token is a Hash-based Message Authentication Code (HMAC) constructed from the request
// and computed by using the SHA256 algorithm, and then encoded by using Base64 encoding.
//
// StringToSign = VERB + "\n" +
// Content-MD5 + "\n" +
// Content-Type + "\n" +
// Date + "\n" +
// CanonicalizedHeaders +
// CanonicalizedResource;
//
NSString* StringToSign = [NSString stringWithFormat:@"%@\n%@\n%@\n%@\n\n",
AZURE_CONNECTION_METHOD.lowercaseString?:@"",
AZURE_RESOURCE_TYPE.lowercaseString?:@"",
AZURE_URL_COLLECTIONS.lowercaseString?:@"",
date.lowercaseString?:@""];
// Generate Key/Message pair
NSData* keyData = [[NSData alloc] initWithBase64EncodedString:AZURE_AUTH_KEY options:NSDataBase64DecodingIgnoreUnknownCharacters];
NSData* messageData = [StringToSign dataUsingEncoding:NSUTF8StringEncoding];
// Encrypt your Key/Message using HMAC SHA256
NSMutableData* HMACData = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, keyData.bytes, keyData.length, messageData.bytes, messageData.length, HMACData.mutableBytes);
// Take your encrypted data, and generate a token that Azure likes.
NSString* signature = [HMACData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSString* unencodedToken = [NSString stringWithFormat:@"type=master&ver=1.0&sig=%@",signature];
NSString* authorizationToken = [unencodedToken stringByReplacingOccurrencesOfString:@"&" withString:@"%26"];
authorizationToken = [authorizationToken stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
return authorizationToken;
}
如果有人遇到类似的间歇性 401 并且能够解决任何帮助,我们将不胜感激。或针对上述代码的调试步骤的建议,我曾尝试将时间戳减少几秒钟,类似的间歇性故障。
尽管在失败时简单地重试几次并减少秒数会导致在 1-2 次重试后得到 200 次响应,但我认为这无论如何都不是理想的解决方案。
感谢您的宝贵时间。
更新:失败的原因请看下面Andrew Liu的解释。我已将他的回复标记为答案,下面是更新后的代码片段。
NSString* unencodedToken = [NSString stringWithFormat:@"type=master&ver=1.0&sig=%@",signature];
// NSString* authorizationToken = [unencodedToken stringByReplacingOccurrencesOfString:@"&" withString:@"%26"];
// authorizationToken = [authorizationToken stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
NSString* authorizationToken = [unencodedToken stringByAddingPercentEncodingWithAllowedCharacters:[[NSCharacterSet characterSetWithCharactersInString:@"&+="] invertedSet]];
return authorizationToken;
我以前见过这个问题,通常它与协议的最后 2 个步骤有关,即 base64 编码很糟糕,或者 URI 编码。调试此问题的一种方法是打印您发送的身份验证令牌,以防失败,并查看是否有任何可能未正确传输的奇怪字符。你可以post这里的越野车令牌,我可以看看。
401(认证失败)通常表示认证令牌有问题。
请务必注意,身份验证令牌是 Base64 编码的字符串 - 这意味着它可以包含 +
字符。
数据库服务器期望身份验证令牌中的 +
个字符被 url 编码(%2B
)...一些但不是所有的 HTTP 客户端将自动编码 HTTP headers 给你。
我怀疑 url-encoding 或将以下变量的 +
转换为 %2B
将解决间歇性 401 问题:
NSString* signature = [HMACData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];