自从我们解决这个问题并成功解决它已经有一段时间了。我想是时候在这里提出答案了。下面的代码属于它自己的类,不能完全开箱即用,但应该能让你在很长一段时间内实现你所需要的。在大多数情况下,这一切都应该可以正常工作,但您只需要确保警报视图、数据存储等各个区域都按照您需要的方式进行设置。
我们理解 Objective-C 和 iOS 处理 NTLM 通信方式的一个主要障碍是弄清楚它与 URL 通信的正常过程。
与 URL 的首次联系是匿名进行的。当然,在 Windows 安全环境中,这将失败。这是应用程序将尝试再次联系该 URL 的时候,但这次使用钥匙串上已经存在的该 URL 的任何凭据并使用 willSendRequestForAuthenticationChallenge 方法。这让我们非常困惑,因为这个方法直到第一次调用失败后才触发。我们终于明白第一个匿名电话是怎么回事。
您将在此处看到的部分模式是,将尝试使用钥匙串上已有的任何凭据进行连接。如果这些失败/丢失,那么我们将弹出一个视图,要求用户输入用户名和密码,然后我们重试。
正如您将在整个代码中看到的那样,我们需要考虑许多特质。它需要多次迭代和大量测试才能达到稳定。其中大部分是基于已经在互联网上发布的模式,这些模式几乎完成了我们试图做的事情,但并没有完全把我们带到那里。
我们所做的代码概括了 GET/POST 调用。这是我在 StackOverflow 上的第一个主要代码帖子,如果我遗漏了一些约定,我深表歉意,我会在引起我注意时纠正我需要做的事情。
#import "MYDataFeeder.h"
#import "MYAppDelegate.h"
#import "MYDataStore.h"
#import "MYAuthenticationAlertView.h"
#import "MYExtensions.h"
@interface MYDataFeeder () <NSURLConnectionDelegate>
@property (strong, nonatomic) void (^needAuthBlock)(NSString *, NSString *);
@property (strong, nonatomic) void (^successBlock)(NSData *);
@property (strong, nonatomic) void (^errorBlock)(NSError *);
@end
@implementation MYDataFeeder{
NSMutableData *_responseData;
NSString *_userName;
NSString *_password;
NSString *_urlPath;
BOOL _hasQueryString;
}
+ (void)get: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
MYDataFeeder *x = [[MYDataFeeder alloc] initWithGetRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
+ (void)post: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
jsonString: (NSString *)jsonString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
MYDataFeeder *x = [[MYDataFeeder alloc] initWithPostRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials jsonString:jsonString successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
- (instancetype)initWithGetRequest: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
return [self initWithRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials isPost:NO jsonString:nil successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
-(instancetype)initWithPostRequest: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
jsonString: (NSString *)jsonString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
return [self initWithRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials isPost:YES jsonString:jsonString successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
//Used for NTLM authentication when user/pwd needs updating
- (instancetype)initWithRequest: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
isPost: (BOOL)isPost
jsonString: (NSString *)jsonString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock //delegate:(id<MYDataFeederDelegate>)delegate
{
self = [super init];
requestString = [requestString stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
if(self) {
if (!errorBlock || !successBlock || !needAuthBlock) {
[NSException raise:@"MYDataFeeder Error" format:@"Missing one or more execution blocks. Need Success, Error, and NeedAuth blocks."];
}
_responseData = [NSMutableData new];
_userName = userName;
_password = password;
_successBlock = successBlock;
_hasNewCredentials = hasNewCredentials;
_errorBlock = errorBlock;
_needAuthBlock = needAuthBlock;
NSString *host = [MYDataStore sharedStore].host; //Get the host string
int port = [MYDataStore sharedStore].port; //Get the port value
NSString *portString = @"";
if (port > 0) {
portString = [NSString stringWithFormat:@":%i", port];
}
requestString = [NSString stringWithFormat:@"%@%@/%@", host, portString, requestString];
NSURL *url = [NSURL URLWithString:requestString];
NSString *absoluteURLPath = [url absoluteString];
NSUInteger queryLength = [[url query] length];
_hasQueryString = queryLength > 0;
_urlPath = (queryLength ? [absoluteURLPath substringToIndex:[absoluteURLPath length] - (queryLength + 1)] : absoluteURLPath);
NSTimeInterval timeInterval = 60; //seconds (60 default)
NSMutableURLRequest *request;
if (isPost) {
request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeInterval];
NSData *requestData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPMethod:@"POST"];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)requestData.length] forHTTPHeaderField:@"Content-Length"];
[request setHTTPBody: requestData];
[request setHTTPShouldHandleCookies:YES];
}
else {
request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:timeInterval];
}
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
return self;
}
- (instancetype)initWithRequest: (NSString *)requestString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock //delegate:(id<MYDataFeederDelegate>)delegate
{
return [self initWithRequest:requestString userName:NULL password:NULL hasNewCredentials:NO isPost:NO jsonString:nil successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock]; //delegate:delegate];
}
#pragma mark - Connection Events
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
return YES;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if (response){
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSInteger code = httpResponse.statusCode;
if (code == 401){
NSLog(@"received 401 response");
[MYAuthenticationAlertView showWithCallback:_needAuthBlock];
[connection cancel];
}
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_successBlock(_responseData);
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[_responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
_errorBlock(error);
}
- (void) connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodNTLM])
{
BOOL hasConnectionCredentials = [[MYDataStore sharedStore] hasConnectionCredentials]; //Determines if there's already credentials existing (see method stub below)
long previousFailureCount = [challenge previousFailureCount];
BOOL hasFailedAuth = NO;
//If the application has already gotten credentials at least once, then see if there's a response failure...
if (hasConnectionCredentials){
//Determine if this URL (sans querystring) has already been called; if not, then assume the URL can be called, otherwise there's probably an error...
if ([[MYDataStore sharedStore] isURLUsed:_urlPath addURL:YES] && !_hasQueryString){
NSURLResponse *failureResponse = [challenge failureResponse];
if (failureResponse){
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)[challenge failureResponse];
long code = [httpResponse statusCode];
if (code == 401){
hasFailedAuth = YES;
}
}
}
}
else{
//Need to get user's credentials for authentication...
NSLog(@"Does not have proper Credentials; possible auto-retry with proper protection space.");
}
/* This is very, very important to check. Depending on how your security policies are setup, you could lock your user out of his or her account by trying to use the wrong credentials too many times in a row. */
if (!_hasNewCredentials && ((previousFailureCount > 0) || hasFailedAuth))
{
NSLog(@"prompt for new creds");
NSLog(@"Previous Failure Count: %li", previousFailureCount);
[[challenge sender] cancelAuthenticationChallenge:challenge];
[MYAuthenticationAlertView showWithCallback:_needAuthBlock];
[connection cancel];
}
else
{
if (_hasNewCredentials){
//If there's new credential information and failures, then request new credentials again...
if (previousFailureCount > 0) {
NSLog(@"new creds failed");
[MYAuthenticationAlertView showWithCallback:_needAuthBlock];
[connection cancel];
} else {
NSLog(@"use new creds");
//If there's new credential information and no failures, then pass them through...
[[challenge sender] useCredential:[NSURLCredential credentialWithUser:_userName password:_password persistence:NSURLCredentialPersistencePermanent] forAuthenticationChallenge:challenge];
}
} else {
NSLog(@"use stored creds");
//...otherwise, use any stored credentials to call URL...
[[challenge sender] performDefaultHandlingForAuthenticationChallenge:challenge];
}
}
}
else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { // server trust challenge
// make sure challenge came from environment host
if ([[MYDataStore sharedStore].host containsString:challenge.protectionSpace.host]) {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
}
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
else {
// request has failed
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
@end
-(BOOL) hasConnectionCredentials
{
NSDictionary *credentialsDict = [[NSURLCredentialStorage sharedCredentialStorage] allCredentials];
return ([credentialsDict count] > 0);
}
//Sample use of Data Feeder and blocks:
-(void)myMethodToGetDataWithUserName:(NSString*)userName password:(NSString*)password{
//do stuff here
[MYDataFeeder get:@"myURL"
userName:userName
password:password
hasNewCredentials:(userName != nil)
successBlock:^(NSData *response){ [self processResponse:response]; }
errorBlock:^(NSError *error) { NSLog(@"URL Error: %@", error); }
needAuthBlock:^(NSString *userName, NSString *password) { [self myMethodToGetDataWithUserName:username withPassword:password]; }
];
}
//The needAuthBlock recalls the same method but now passing in user name and password that was queried from within an AlertView called from within the original DataFeeder call