2017-03-16 4 views
2

Ich versuche, HTTP Basic Auth mit NSURLSession zu implementieren, aber ich stoße auf mehrere Probleme. Bitte lesen Sie die gesamte Frage, bevor Sie antworten, ich bezweifle, dass dies ein Duplikat einer anderen Frage ist.HTTP Basic Auth mit NSURLSession

Nach den Tests, die ich laufen habe, das Verhalten von NSURLSession ist folgende:

  • Die erste Anforderung wird immer gemacht, ohne die Authorization-Header.
  • Wenn die erste Anfrage mit einer 401 Unauthorized Antwort und einem WWW-Authenticate Basic realm=... Header fehlschlägt, wird es automatisch erneut versucht.
  • Bevor die Anfrage erneut versucht wird, versucht die Sitzung, Anmeldeinformationen zu erhalten, indem sie in die NSURLCredentialStorage der Sitzungskonfiguration oder durch den Aufruf der Delegiertenmethode URLSession:task:didReceiveChallenge:completionHandler: (oder beides) sucht.
  • Wenn Anmeldeinformationen erhalten werden können, wird die Anforderung mit dem richtigen Header Authorization wiederholt. Wenn nicht, wird es ohne den Header wiederholt (was seltsam ist, da es sich in diesem Fall genau um dieselbe Anfrage handelt).
  • Wenn die zweite Anforderung erfolgreich ist, wird die Aufgabe transparent als erfolgreich gemeldet und Sie werden nicht einmal benachrichtigt, dass die Anforderung zweimal versucht wurde. Wenn nicht, wird der Fehler der zweiten Anfrage gemeldet (aber nicht der erste).

Das Problem, das ich mit diesem Verhalten ist, dass ich große Dateien auf meinen Server über mehrteilige Anfragen bin das Hochladen, so, wenn die Anforderung zweimal versucht wird, der gesamte POST Körper wird zweimal gesendet, die eine schreckliche Overhead ist.

Ich habe versucht, die Authorization-Header an die httpAdditionalHeaders der Sitzungskonfiguration manuell hinzufügen, aber es funktioniert nur, wenn die Eigenschaft festgelegt ist vor die Sitzung erstellt wird. Der Versuch, danach session.configuration.httpAdditionalHeaders zu ändern, funktioniert nicht. Auch die Dokumentation sagt eindeutig, dass der Authorization Header nicht manuell gesetzt werden sollte.


So ist meine Frage: Wenn ich die Sitzung starten müssen, bevor ich die Anmeldeinformationen erhalten, und wenn ich sicher sein wollen, dass die Anfragen werden immer mit dem richtigen Authorization Header das erste Mal gemacht, wie kann ich machen ?


Hier ist ein Codebeispiel, das ich für meine Tests verwendet habe. Sie können alle oben beschriebenen Verhaltensweisen damit reproduzieren.

Beachten Sie, dass Sie Ihren eigenen HTTP-Server verwenden wil müssen entweder um in der Lage sein, die doppelten Anfragen zu sehen, und die Anfragen zu protokollieren oder ein Proxy-Verbindung über die alle Anforderungen protokolliert (I Charles Proxy dafür verwendet habe)

class URLSessionTest: NSObject, URLSessionDelegate 
{ 
    static let shared = URLSessionTest() 

    func start() 
    { 
     let requestURL = URL(string: "https://httpbin.org/basic-auth/username/password")! 
     let credential = URLCredential(user: "username", password: "password", persistence: .forSession) 
     let protectionSpace = URLProtectionSpace(host: "httpbin.org", port: 443, protocol: NSURLProtectionSpaceHTTPS, realm: "Fake Realm", authenticationMethod: NSURLAuthenticationMethodHTTPBasic) 

     let useHTTPHeader = false 
     let useCredentials = true 
     let useCustomCredentialsStorage = false 
     let useDelegateMethods = true 

     let sessionConfiguration = URLSessionConfiguration.default 

     if (useHTTPHeader) { 
      let authData = "\(credential.user!):\(credential.password!)".data(using: .utf8)! 
      let authValue = "Basic " + authData.base64EncodedString() 
      sessionConfiguration.httpAdditionalHeaders = ["Authorization": authValue] 
     } 
     if (useCredentials) { 
      if (useCustomCredentialsStorage) { 
       let urlCredentialStorage = URLCredentialStorage() 
       urlCredentialStorage.set(credential, for: protectionSpace) 
       sessionConfiguration.urlCredentialStorage = urlCredentialStorage 
      } else { 
       sessionConfiguration.urlCredentialStorage?.set(credential, for: protectionSpace) 
      } 
     } 

     let delegate = useDelegateMethods ? self : nil 
     let session = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil) 

     self.makeBasicAuthTest(url: requestURL, session: session) { 
      self.makeBasicAuthTest(url: requestURL, session: session) { 
       DispatchQueue.main.asyncAfter(deadline: .now() + 61.0) { 
        self.makeBasicAuthTest(url: requestURL, session: session) {} 
       } 
      } 
     } 
    } 

    func makeBasicAuthTest(url: URL, session: URLSession, completion: @escaping() -> Void) 
    { 
     let task = session.dataTask(with: url) { (data, response, error) in 
      if let response = response { 
       print("response : \(response)") 
      } 
      if let data = data { 
       if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) { 
        print("json : \(json)") 
       } else if data.count > 0, let string = String(data: data, encoding: .utf8) { 
        print("string : \(string)") 
       } else { 
        print("data : \(data)") 
       } 
      } 
      if let error = error { 
       print("error : \(error)") 
      } 
      print() 
      DispatchQueue.main.async(execute: completion) 
     } 
     task.resume() 
    } 

    @objc(URLSession:didReceiveChallenge:completionHandler:) 
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) 
    { 
     print("Session authenticationMethod: \(challenge.protectionSpace.authenticationMethod)") 
     if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) { 
      let credential = URLCredential(user: "username", password: "password", persistence: .forSession) 
      completionHandler(.useCredential, credential) 
     } else { 
      completionHandler(.performDefaultHandling, nil) 
     } 
    } 

    @objc(URLSession:task:didReceiveChallenge:completionHandler:) 
    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) 
    { 
     print("Task authenticationMethod: \(challenge.protectionSpace.authenticationMethod)") 
     if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) { 
      let credential = URLCredential(user: "username", password: "password", persistence: .forSession) 
      completionHandler(.useCredential, credential) 
     } else { 
      completionHandler(.performDefaultHandling, nil) 
     } 
    } 
} 

Anmerkung 1: Wenn mehrere Anfragen in einer Reihe auf dem gleichen Endpunkt zu machen, habe ich das Verhalten oben genannten Bedenken die erste Anforderung nur beschrieben habe. Nachfolgende Anforderungen werden beim ersten Mal mit dem richtigen Header Authorization ausprobiert. Wenn Sie jedoch einige Zeit warten (etwa 1 Minute), kehrt die Sitzung zum Standardverhalten zurück (die erste Anforderung wurde zweimal versucht).

Anmerkung 2: Dies ist nicht direkt verwandt, aber NSURLCredentialStorage für die urlCredentialStorage der Sitzungskonfiguration Verwendung eines benutzerdefinierten scheint nicht zu funktionieren. Verwenden Sie nur den Standardwert (der die gemeinsame NSURLCredentialStorage gemäß der Dokumentation ist).

Anmerkung 3: Ich habe versucht, mit Alamofire, aber da auf NSURLSession basiert es, verhält es sich genau in der gleichen Art und Weise.

+0

Warum nicht immer die erste Anfrage keine andere Arbeit als einen richtigen Authorization-Header bekommen? Die nächste Anfrage (und weiter) macht das ganze schwere Heben. – GlennRay

+0

Versuchen Sie, nicht zu überschreiben 'didReceiveChallenge' – RunLoop

+0

@GlennRay Das ist eine hässliche Lösung, die ich nicht einmal in Betracht ziehen möchte. Immer noch verbraucht Bandbreite ohne triftigen Grund und wie gesagt, es ist nicht nur die allererste Anfrage der Sitzung, sondern die erste Anfrage jedes Mal, wenn Sie etwa 1min bleiben, ohne etwas zu senden, so dass auch schrecklich unpraktisch ist, implementieren. – deadbeef

Antwort

2

Wenn möglich, sollte der Server lange bevor der Client das Senden des Nachrichtentexts beendet mit einem Fehler antworten. In vielen serverseitigen High-Level-Sprachen ist dies jedoch schwierig, und es gibt keine Garantie dafür, dass der Upload auch dann gestoppt wird, wenn Sie dies tun.

Das eigentliche Problem ist, dass Sie einen großen Upload mit einer einzigen POST-Anfrage durchführen. Dies macht die Authentifizierung problematisch und verhindert auch jede Art von nützlicher Fortsetzung von Uploads, wenn die Verbindung während des Hochladens unterbrochen wird. die Upload-Chunking löst im Grunde alle Ihre Fragen:

  • Für Ihre erste Anforderung, nur die Menge schicken, die ohne zusätzliche Ethernet-Pakete, also berechnen typische Kopfgröße, mod von 1500 Bytes passt, fügen Sie ein paar Zehner von Bytes für gutes Maß, subtrahieren Sie von 1500, und hard-code diese Größe für Ihren ersten Brocken. Sie haben höchstens ein paar Pakete verschwendet.

  • Für nachfolgende Stücke, die Größe hochdrehen.

  • Wenn eine Anfrage fehlschlägt, fragen Sie den Server, wie viel er bekommen hat, und versuchen Sie es erneut dort, wo der Upload abgebrochen wurde.

  • Erteilen Sie eine Anforderung, um dem Server mitzuteilen, dass Sie den Upload abgeschlossen haben.

  • Löschen Sie regelmäßig partielle Uploads auf der Serverseite mit einem Cron-Job oder was auch immer.

Das heißt, wenn Sie eine authentifizierte GET-Anfrage direkt vor Ihren POST-Anforderung gesendet nicht die Kontrolle über die Server-Seite haben, ist die übliche Abhilfe. Dies minimiert Verschwendung von Paketen, während es immer noch größtenteils funktioniert, solange das Netzwerk zuverlässig ist.

+0

Vielen Dank für Ihre Antwort. Ich mache bereits Chunked-Uploads, aber Chunks selbst sind immer noch groß. Für jetzt habe ich einen Weg gefunden, die richtigen Header in jede Anfrage zu injizieren, damit es funktioniert. Ich hoffte auf eine Lösung auf der 'URLSession'-Ebene. Ich finde es sehr überraschend, dass ich die Authentifizierung nicht im Voraus konfigurieren kann und stattdessen auf eine fehlgeschlagene Anfrage warten muss. Ich habe das Gefühl, etwas an dieser API verpasst zu haben. – deadbeef

+0

Warum also nicht einfach den ersten Chunk so konfigurieren, dass er winzig ist - sagen wir 500 Bytes? – dgatwood

+0

Chunks sind in der Größe festgelegt und ich möchte den Server-Code wegen eines iOS-Fehlers nicht neu schreiben :) – deadbeef