Swift中的HTTP(十五) 自動鑑權

語言: CN / TW / HK

HTTP簡介

HTTP基礎結構

HTTP請求體

HTTP 載入請求

HTTP 模擬測試

HTTP 鏈式載入器

HTTP 動態修改請求

HTTP 請求選項

HTTP 重置

HTTP 取消

HTTP 限流

HTTP 重試

HTTP 基礎鑑權

HTTP 自動鑑權設定

HTTP 自動鑑權

HTTP 複合載入器

HTTP 頭腦風暴

HTTP 總結

上一篇文章介紹了 OAuth 流程的基礎知識:我們如何檢查令牌、我們如何要求使用者登入、我們如何重新整理令牌等等。 在這篇文章中,我們將採用該狀態機並將其整合到 HTTPLoader 子類中。

載入器

我們已經為授權流程定義了狀態機,但我們需要另一個簡單的狀態機來描述載入器將如何與之互動。 讓我們考慮一下我們的載入器在被要求載入任務時可能處於的各種“狀態”:

  • 空閒(什麼都沒有發生,或者狀態機失敗)→我們應該啟動狀態機並開始執行授權流程
  • 授權(狀態機正在執行)→我們應該讓任務等待狀態機完成
  • 授權(我們有有效的憑據)→載入任務
  • 授權(我們的憑證已過期)→ 我們需要重新整理令牌 如果我們仔細觀察一下,我們會發現“空閒”狀態實際上與“授權 + 過期令牌”狀態是一回事:在任何一種情況下,我們都需要啟動狀態機,以便 我們可以獲得新的令牌(回想一下,狀態機已經具有重新整理過期令牌的邏輯)。 考慮到這一點,讓我們存根我們的載入器: ``` public class OAuth: HTTPLoader {

    private var stateMachine: OAuthStateMachine? private var credentials: OAuthCredentials? private var pendingTasks = Array()

    public override func load(task: HTTPTask) { // TODO: make everything threadsafe

    if stateMachine != nil {
        // "AUTHORIZING" state
        // we are running the state machine; load this task later
        self.enqueueTask(task)
    
    } else if let tokens = credentials {
        // we are not running the state machine
        // we have tokens, but they might be expired
        if tokens.expired == true {
            // "AUTHORIZED+EXPIRED" state
            // we need new tokens
            self.enqueueTask(task)
            self.runStateMachine()
        } else {
            // "AUTHORIZED+VALID" state
            // we have valid tokens!
            self.authorizeTask(task, with: tokens)
            super.load(task: task)
        }
    
    } else {
        // "IDLE" state
        // we are not running the state machine, but we also do not have tokens
        self.enqueueTask(task)
        self.runStateMachine()
    }
    

    }

} ```

我們可以看到 if 語句中編碼的四種可能狀態。 我們遺漏了一些部分,所以讓我們看一下: ``` public class OAuth: HTTPLoader { ... // the stuff above

private func authorizeTask(_ task: HTTPTask, with credentials: OAuthCredentials) {
    // TODO: create the "Authorization" header value
    // TODO: set the header value on the task
}

private func enqueueTask(_ task: HTTPTask) {
    self.pendingTasks.append(task)
    // TODO: how should we react if the task is cancelled while it's pending?
}

private func runStateMachine() {
    self.stateMachine = OAuthStateMachine(...)
    self.stateMachine?.delegate = self
    self.stateMachine?.run()
}

}

extension OAuth: OAuthStateMachineDelegate {

// TODO: the OAuth loader itself needs a delegate for some of these to work

func stateMachine(_ machine: OAuthStateMachine, wantsPersistedCredentials: @escaping (OAuthCredentials?) -> Void) {
    // The state machine is asking if we have any credentials
    // TODO: if self.credentials != nil, use those
    // TODO: if self.credentials == nil, ask a delegate
}

func stateMachine(_ machine: OAuthStateMachine, persistCredentials: OAuthCredentials?) {
    // The state machine has found new tokens for us to save (nil = delete tokens)
    // TODO: save them to self.credentials
    // TODO: also pass them on to our delegate
}

func stateMachine(_ machine: OAuthStateMachine, displayLoginURL: URL, completion: @escaping (URL?) -> Void) {
    // The state machine needs us to display a login UI
    // TODO: pass this on to our delegate
}

func stateMachine(_ machine: OAuthStateMachine, displayLogoutURL: URL, completion: @escaping () -> Void) {
    // The state machine needs us to display a logout UI
    // This happens when the loader is reset. Some OAuth flows need to display a webpage to clear cookies from the browser session
    // However, this is not always necessary. For example, an ephemeral ASWebAuthenticationSession does not need this
    // TODO: pass this on to our delegate
}

func stateMachine(_ machine: OAuthStateMachine, didFinishWithResult result: Result<OAuthCredentials, Error>) {
    // The state machine has finished its authorization flow

    // TODO: if the result is a success
    //       - save the credentials to self.credentials (we should already have gotten the "persistCredentials" callback)
    //       - apply these credentials to everything in self.pendingTasks
    // 
    // TODO: if the result is a failure
    //       - fail all the pending tasks as "cannot authenticate" and use the error as the "underlyingError"

    self.stateMachine = nil
}

} ```

大多數對狀態機的反應都涉及將資訊轉發給另一個代表。 這是因為我們的載入器(正確!)不知道如何顯示登入/登出 UI,我們的載入器也不知道憑據如何儲存或儲存在何處。 這是應該的。 顯示 UI 和持久化資訊與我們的載入器“驗證請求”的任務無關。

重置

除了 TODO: 分散在我們程式碼周圍的專案之外,我們缺少的最後一個主要難題是“重置”邏輯。 乍一看,我們可能會認為是這樣的:

public func reset(with group: DispatchGroup) { self.stateMachine?.reset(with: group) super.reset(with: group) }

正如上一篇文章中所討論的,狀態機中的每個狀態都可以被 reset() 呼叫中斷,這就是發生這種情況的方式。 因此,如果我們的機器當前正在執行,這就是我們可以中斷它的方式。

……但是如果它沒有執行呢? 如果我們已經通過身份驗證並擁有有效令牌,然後我們收到對 reset() 的呼叫怎麼辦? (這實際上是常見的情況,因為“重置”在很大程度上類似於“登出”,通常只有在身份驗證成功時才會發生)

在這種情況下,我們需要修改我們的狀態機。 回想一下我們上次描述這個 OAuth 流程:

image.png

此流程中沒有任何內容可處理“登出”場景。 我們需要稍微修改一下,以便我們也有辦法使令牌無效。 此登出狀態已在之前的“注意事項”部分中列出。 包含它後,狀態流程圖現在大致如下所示:

image.png

關於這件事需要注意的兩點是:

  • 從所有先前狀態到新“登出”狀態的虛線表示在狀態機執行時通過呼叫 reset() 來“中斷”該狀態
  • 新的“登出”狀態是狀態機的可能入口點。 也就是說,我們可以在這個狀態下啟動機器。 我將把“登出”狀態的實現留給你,但它需要做一些事情:

  • 它需要構造 URL 來顯示“登出”頁面以顯示給使用者(之前提到的從瀏覽器會話中清除 cookie 的頁面)

  • 它需要聯絡伺服器並告訴他們憑據已被撤銷
  • 它需要通知其委託人清除任何持久化的憑據 有了這個,我們的 OAuth 載入器應該可以完全正常工作: ``` public func reset(with group: DispatchGroup) { if let currentMachine = self.stateMachine { // we are currently authorizing; interrupt the flow currentMachine.reset(with: group) } else { // TODO: you'll want to pass the "group" into the machine here self.stateMachine = OAuthStateMachine(...) self.stateMachine?.delegate = self
    // "running" the state machine after we gave it the DispatchGroup should start it in the LogOut state
    self.stateMachine?.run()
    

    } super.reset(with: group) } ```

結論

我希望這兩篇文章說明 OAuth 不必是這麼可怕的東西。 我們有狀態機來授權(或取消授權)使用者,它有六種可能的狀態。 這不是很多,我們可以把它記在腦子裡。 同樣,載入程式本身只有少數幾種可能的狀態,具體取決於狀態機的情況。 通過將各自的邏輯封裝在不同的抽象層中,我們能夠將整體複雜性保持在相當低的水平。 我們機器的每個狀態子類都是直截了當的; 我們的 StateMachine 類中幾乎沒有程式碼; 甚至我們的 OAuth 載入程式也只有幾十行。

但由此,我們最終得到了一個功能齊全的 OAuth 流程:

  • 我們保證一次只執行一個 OAuth 授權 UI
  • 我們允許客戶顯示他們想要的 OAuth UI
  • 我們允許客戶以他們想要的方式保留令牌
  • 我們允許中斷授權
  • 我們允許通過重置取消授權 太棒了!

在下一篇文章中,我們將把 BasicAuth 和 OAuth 載入器組合成一個複合身份驗證載入器。