2017-03-16 180 views
2

我試圖與NSURLSession實現HTTP基本身份驗證,但我遇到了幾個問題。請在回答之前閱讀完整個問題,我懷疑這是其他問題的重複。與NSURLSession的HTTP基本身份驗證

據我已經運行測試,NSURLSession行爲如下:

  • 的第一個請求沒有Authorization頭總是讓。
  • 如果第一個請求失敗並返回401 Unauthorized響應和WWW-Authenticate Basic realm=...標頭,它將自動重試。
  • 在重試請求之前,會話將嘗試通過查看會話配置的NSURLCredentialStorage或通過調用代理方法(或兩者)來獲取憑證。
  • 如果可以獲取證書,則使用正確的Authorization標題重試請求。如果沒有,它會重試沒有標題(這很奇怪,因爲在這種情況下,這是完全相同的請求)。
  • 如果第二個請求成功,則該任務將被透明地報告爲成功,並且您甚至不會通知該請求已嘗試兩次。如果不是,則報告第二個請求失敗(但不是第一個)。

我有這種行爲的問題是,我上傳大文件,通過多部分請求我的服務器,所以當請求嘗試兩次,整個POST體發送兩次,這是一個可怕的開銷。

我試圖給Authorization頭手動添加到會話配置的httpAdditionalHeaders,但前提是該屬性設置在創建會話之前它的工作原理。試圖以後修改session.configuration.httpAdditionalHeaders不起作用。此外,文檔中明確指出,不應手動設置標頭Authorization


所以我的問題是:如果我必須先獲得憑據啓動會話,如果我想以確保請求始終用正確的Authorization頭在第一時間作出,我該怎麼辦做什麼?


這是我用於測試的代碼示例。你可以重現上面描述的所有行爲。

注意的是,爲了能夠看到你西港島線需要可以使用自己的HTTP服務器和日誌的請求或連接通過記錄所有請求的代理的雙重要求(我已經爲這個使用Charles Proxy

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) 
     } 
    } 
} 

注1:當進行連續多次請求到同一個端點,行爲上面我只關注第一個請求描述。隨後的請求首次嘗試使用正確的Authorization標頭。但是,如果您等待一段時間(大約1分鐘),會話將返回到默認行爲(第一次請求嘗試兩次)。

注2:這是沒有直接關係,但使用自定義NSURLCredentialStorage爲會話配置的urlCredentialStorage似乎並沒有工作。只有使用默認值(根據文檔共享NSURLCredentialStorage)纔有效。

注3:我已經使用Alamofire試過,但由於它是基於NSURLSession,它在完全相同的方式表現。

+0

爲什麼沒有第一個請求總是沒有工作,除非得到一個適當的授權頭?接下來的請求(以及之後)完成所有繁重的工作。 – GlennRay

+0

儘量不要重寫'didReceiveChallenge' – RunLoop

+0

@GlennRay這是一個醜陋的解決方案,我甚至不想考慮。依然沒有充分的理由消耗帶寬,正如我所說的那樣,這不僅僅是會話的第一次請求,而是每次在沒有發送任何東西的情況下停留約1分鐘時的第一個請求,所以這也是非常不切實際的沒有實現。 – deadbeef

回答

2

如果可能,服務器應該在客戶端完成發送主體之前很久纔會響應一個錯誤。但是,在許多高級別的服務器端語言中,這很困難,即使您這樣做,也無法保證上傳會停止。

真正的問題是,您正在使用單個POST請求執行大量上載。這會使認證有問題,並且如果連接在上傳過程中中斷,則也會阻止任何有用的上傳繼續。分塊上傳基本上解決了所有的問題:

  • 爲了您的第一個請求,只發送,將適合無需添加額外的以太網數據包的量,即計算典型的頭大小,用1500個字節國防部,添加一些好幾十個字節,從1500中減去,併爲你的第一個塊硬編碼。最多你浪費了幾個包。

  • 對於後面的塊,請將尺寸向上調整。

  • 當請求失敗時,詢問服務器得到了多少,然後從上傳停止的位置重試。

  • 發出請求,告訴服務器何時完成上傳。

  • 定期清除服務器端的部分上傳與cron作業或任何其他。

也就是說,如果你沒有在服務器端控制,通常的解決辦法是你的POST請求之前有權發送的驗證的GET請求。這可以最大限度地減少浪費的數據包,但只要網絡可靠,大多數工作仍在進行。

+0

謝謝你的回答。我已經在分塊上傳,但塊本身仍然很大。現在我找到了一種方法將適當的頭部注入到每個請求中,以便它能夠正常工作。我希望在URLSession級別有一個解決方案。我覺得很奇怪,我無法配置身份驗證,而是必須等待請求失敗。我覺得我錯過了這個API的一些東西。 – deadbeef

+0

那麼,爲什麼不配置第一塊很小 - 比如500字節呢? – dgatwood

+0

塊大小固定,我不想因iOS錯誤而重寫服務器端代碼:) – deadbeef