Swift中的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 流程:
此流程中沒有任何內容可處理“登出”場景。 我們需要稍微修改一下,以便我們也有辦法使令牌無效。 此登出狀態已在之前的“注意事項”部分中列出。 包含它後,狀態流程圖現在大致如下所示:
關於這件事需要注意的兩點是:
- 從所有先前狀態到新“登出”狀態的虛線表示在狀態機執行時通過呼叫 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 載入器組合成一個複合身份驗證載入器。