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 加载器组合成一个复合身份验证加载器。