0

NTLM 인증을 사용하여 평생 동안 다양한 웹 서비스 호출을해야하는 "전통적인"엔터프라이즈 iPad 응용 프로그램이 있습니다. 응용 프로그램을 시작할 때 키 체인에 사용자 이름과 암호를 가져 오는 것이 예상됩니다 (키 체인에 사용자 이름이 없으므로 처음 사용하는 응용 프로그램을 저장하고이어서 암호가 작동하지 않아 업데이트됩니다). 업데이트).NTLM 웹 서비스 호출에 적합한 NSURLConnection/Credential 패턴은 무엇입니까?

시작시 응용 프로그램의 초기 데이터를 얻으려면 다양한 웹 서비스 호출이 필요합니다. 그런 다음 사용자는 원하는 기능을 선택하기위한 탭 컨트롤러를 제공 받게되며 물론 원하는 웹 서비스 호출을 수행 할 수 있습니다.

이 StackOverflow 응답 (How do you return from an asynchronous NSURLConnection to the calling class?)에 제시된대로 사용자 지정 데이터 대리자를 통해 데이터를받는 각 클래스를 처리하기위한 전술이 있다고 생각합니다. 그러나 나는 여전히 제대로 기능을 사용하는 방법에 대해 다소 혼란 스럽다. didReceiveAuthenticationChallenge에서

, 나는이

[[challenge sender] useCredential:[NSURLCredential credentialWithUser:@"myusername" 
          password:@"mypassword" 
         persistence:NSURLCredentialPersistencePermanent] 
     forAuthenticationChallenge:challenge]; 

과 같은 코드가 나는 영원한 지속성을 설정하고있어 이후로, 내가 지속적으로 기능에서 사용자 이름과 암호를 통과해야하지 기대합니다. 처음에 사용자의 NTLM 자격 증명을 설정하는 데 사용되는 패턴이 있습니까 (그리고/또는 이미 존재하는지 확인). 그런 다음 웹 서비스 호출에 영구적 인 자격 증명을 사용하면됩니까?

또한이 문제에 대한 2 차 질문으로 Objective-C 응용 프로그램 전체에 사용자 이름/암호를 전달하는 적절한/우아한 접근 방식은 무엇입니까? 나는 글로벌 var 또는 singleton 인스턴스를 생각하고있다. (필요한 몇 개의 var에 대해 약간 과장된 것으로 보인다).

답변

0

우리가이 문제를 해결하고 성공적으로 해결 한 이래로 얼마간이되어 왔습니다. 나는 여기에 답을 할 시간이라고 생각했다. 아래 코드는 자체 클래스에 속해 있으며 기본적으로 제대로 작동하지 않지만 필요한 부분에 대해 긴 방법을 제공해야합니다. 대부분의 경우 잘 작동하지만 경고보기, 데이터 저장소 등과 같은 다양한 영역이 필요한 방식으로 설정되어 있는지 확인해야합니다.

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 
+0

전체 파일을 가져올 수 있습니까? – dip

+0

죄송합니다. 이전에 귀하의 요청을 보지 못했습니다! 나는 정확한 회사 (그리고 잠재적으로 "민감한") 파일을 제공하지 않도록 상당히 포괄적 인 예를 보여 주려고 노력했다. 나는 "작업하는"파일 세트를 정리하려고 노력할 것이지만, 나는 그 순간에 다른 노력들로 인해 꽤 혼란 스러울 때까지 약간의 시간이 걸릴 것이다 (나의 겸손한 사과). – Prethen