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 加載器組合成一個複合身份驗證加載器。