阅读视图

发现新文章,点击刷新页面。

TCP/UDP介绍及区别

传输控制协议(TCP)驱动着可靠数据的传输。相较之下,用户数据包协议(UDP)优先于速度和效率,这一点对网络操作至关重要。 TCP和UDP协议是互联网的功能支柱,能将不同类型的的数据从一个网络资源传输

Swift中的HTTP(十八) 总结

在这个系列的过程中,我们从一个简单的想法开始,并将它带到了一些非常迷人的地方。 我们开始的想法是可以将网络层抽象为“我发送此请求,最终我得到响应”的想法。 在阅读 Rob Napier 关于协议协议的

Swift中的HTTP(十六)复合加载器

到目前为止,我们已经构建了两个不同的加载器来处理身份验证,并且可以想象我们想要构建更多来支持其他加载器。 如果我们可以将所有“身份验证”逻辑封装到一个加载程序中,那不是很好吗? 我们将通过创建一个复合

Swift中的HTTP(十五) 自动鉴权

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<HTTPTask>()

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

Swift中的HTTP(十三) 基础鉴权

HTTP简介

HTTP基础结构

HTTP请求体

HTTP 加载请求

HTTP 模拟测试

HTTP 链式加载器

HTTP 动态修改请求

HTTP 请求选项

HTTP 重置

HTTP 取消

HTTP 限流

HTTP 重试

HTTP 基础鉴权

HTTP 自动鉴权设置

HTTP 自动鉴权

HTTP 复合加载器

HTTP 头脑风暴

HTTP 总结

对 web api 的 HTTP 请求通常需要有某种凭据。 最简单的身份验证类型是基本访问身份验证,在这篇文章中,我们将把这个功能添加到我们的库中。

当我们阅读基本身份验证的规范(或维基百科文章)时,我们看到它只是添加了一个授权标头,以及一个 base64 编码的用户名和密码。 添加标头值是我们之前见过的,因此我们的 BasicAuth 加载器在原则上与我们的 ApplyEnvironment 加载器相似。

配置

我们需要的第一件事是一个结构来表示用户名和密码:

public struct BasicCredentials: Hashable, Codable {
    public let username: String
    public let password: String

    public init(username: String, password: String) { ... }
}

我们还需要一个 HTTPRequestOption 以便单个请求可以指定唯一的凭据:

extension BasicCredentials: HTTPRequestOption {
    public static let defaultOptionValue: BasicCredentials? = nil
}

extension HTTPRequest {
    public var basicCredentials: BasicCredentials? {
        get { self[option: BasicCredentials.self] }
        set { self[option: BasicCredentials.self] = newValue }
    }
}

顺便说一句,让我们转向加载器。

加载器

加载器的行为很简单:当请求进来时,加载器会查看它是否指定了自定义凭据。 如果是这样,它将它们转换为正确的授权标头并将请求发送到链中。 如果没有,它会将任何本地存储的凭据应用于请求,然后继续发送。 如果它没有任何凭据,它会在检索一些请求时暂停传入请求。

让我们把它存根:

public protocol BasicAuthDelegate: AnyObject {
    func basicAuth(_ loader: BasicAuth, retrieveCredentials callback: @escaping (BasicCredentials?) -> Void)
}

public class BasicAuth: HTTPLoader {
    public weak var delegate: BasicAuthDelegate?

    private var credentials: BasicCredentials?
    private var pendingTasks = Array<HTTPTask>()

    public override func load(task: HTTPTask) {
        if let customCredentials = task.request.basicCredentials {
            self.apply(customCredentials, to: task)
        } else if let mainCredentials = credentials {
            self.apply(mainCredentials, to: task)
        } else {
            self.pendingTasks.append(task)
            // TODO: ask delegate for credentials
        }
    }

    private func apply(_ credentials: BasicCredentials, to task: HTTPTask) {
        let joined = credentials.username + ":" + credentials.password
        let data = Data(joined.utf8)
        let encoded = data.base64EncodedString()
        let header = "Basic (encoded)"
        task.request[header: "Authorization"] = header
    }
}

这是基本的实现,但不太正确。 例如,如果同时收到许多请求,我们最终会多次向委托人询问凭据。 我们需要在管理某些状态方面做得更好:

public class BasicAuth: HTTPLoader {
    public weak var delegate: BasicAuthDelegate?

    private enum State {
        case idle
        case retrievingCredentials(Array<HTTPTask>)
        case authorized(BasicCredentials)
    }

    private var state = State.idle

    public override func load(task: HTTPTask) {
        if let customCredentials = task.request.basicCredentials {
            self.apply(customCredentials, to: task)
            super.load(task: task)
            return
        } 

        // TODO: make this threadsafe
        switch state {
            case .idle:
                // we need to ask for credentials
                self.state = .retrievingCredentials([task])
                self.retrieveCredentials()

            case .retrievingCredentials(let others):
                // we are currently asking for credentials and waiting for the delegate
                self.state = .retrievingCredentials(others + [task])

            case .authorized(let credentials):
                // we have credentials
                self.apply(credentials, to: task)
                super.load(task: task)
        }
    }

    private func retrieveCredentials() {
        if let d = delegate {
            // we've got a delegate! Ask it for credentials
            // these credentials could come from storage (eg, the Keychain), from a UI ("log in" page), or somewhere else
            // that decision is entirely up to the delegate
            DispatchQueue.main.async {
                d.basicAuth(self, retrieveCredentials: { self.processCredentials($0) })
            }
        } else {
            // we don't have a delegate. Assume "nil" credentials
            self.processCredentials(nil)
        }
    }

    private func processCredentials(_ retrieved: BasicCredentials?) {
        // TODO: make this threadsafe

        guard case .retrievingCredentials(let pending) = state else {
            // we got credentials, but weren't waiting for them; do nothing
            // this could happen if we were "reset()" while waiting for the delegate
            return
        }

        if let credentials = retrieved {
            state = .authorized(credentials)
            for task in pending {
                self.apply(credentials, to: task)
                super.load(task: task)
            }
        } else {
            // we asked for credentials but didn't get any
            // all of these tasks will fail
            state = .idle
            pending.forEach { $0.fail(.cannotAuthenticate) }
        }
    }

    private func apply(_ credentials: BasicCredentials, to task: HTTPTask) {
        ...
    }
}

这看起来好多了,但它仍然缺少一些关键的东西,我将留给你来实现: 我们需要注意一个任务在挂起时被取消(即 .retrievingCredentials 状态) 这些都不是线程安全的 我们需要 reset(with:) 逻辑来使挂起的任务失败并返回到 .idle 状态 还有一些其他场景值得考虑:

  • 此加载程序假定请求将始终经过身份验证。 您将如何更改它以允许请求完全绕过身份验证,因为他们不需要它?

  • 在 URLComponents 上有一种方法可以指定用户和密码。 由于 HTTPRequest 在后台使用 URLComponents,您如何更改此加载器以在那里查找可能的授权信息,而不是(或除此之外)basicCredentials 请求选项? 你认为这应该是 API 吗? 为什么或者为什么不?

  • 此加载程序不会检测请求何时未通过身份验证,例如返回 401 Unauthorized 或 403 Forbidden 的响应。 这个装载机应该尝试检测吗? 为什么或者为什么不?

正如我们所见,基本身份验证归结为向我们的传出请求简单添加一个标头。 通过延迟传入请求,我们可以转身询问应用程序的另一部分(通过代理)是否有任何凭证供我们使用。

这种模式将在下一篇文章中继续为我们服务,届时我们将研究通过 OAuth 2.0 进行身份验证的更复杂的场景。

Swift中的HTTP(十二) 重试

如果收到的响应与客户端所寻找的不完全一致,大多数网络库都能够自动再次发送请求。 让我们也把它添加到我们的库中。 配置 回忆一下我们的 HTTPLoader 的 API: 请记住,这定义了一种加载任务的
❌