阅读视图
iOS响应式编程Combine——简介
TCP/UDP介绍及区别
Swift中的HTTP(十八) 总结
Swift中的HTTP(十七) 头脑风暴
我原计划在不同的加载器上发布更多的帖子,你可以使用这个架构构建,但是为了“完成”这个系列,我决定放弃一个帖子每个加载器,而是强调要点 其中一些。
OpenID
我们已经了解了如何实现通过 OAuth 2 流授权请求的加载程序,但在其之上存在一个抽象,称为 OpenID。 使用 OAuth 加载程序,我们需要指定登录 url、刷新令牌的 url 等内容。 OpenID 允许身份提供者通过发送包含所有这些 url(以及协议的其他特性)的清单来抽象化这一点。
如果我们想自己实现 OpenID,我们需要在我们的状态机中有一个初步状态来首先获取这个清单,然后将其用作后续状态逻辑的基础。 或者,我们可以将 OpenID 的现有实现(例如官方实现)包装在自定义 HTTPLoader 子类中,并允许该库执行复杂的逻辑。 在这种情况下,我们的 HTTPLoader 子类将充当库提供的 API 与 HTTP 加载程序链所需的 API 之间的适配器。
缓存
从概念上讲,缓存加载器应该相对简单易懂。 当一个请求进入这个加载器时,它会检查请求(可能还有一个 HTTPRequestOption 来指示是否允许缓存)并查看它是否与任何已保存的响应(在内存中、磁盘上等)相匹配。 如果存在这样的响应,则加载程序返回该响应,而不是将请求进一步发送到链中。
如果响应不存在,它会继续执行典型的请求,但也会插入另一个完成处理程序,以便它可以捕获响应,并且(如果满足正确的条件)将其持久化以用于未来的请求。
重复备份
重复备份类似于缓存,因为当一个请求进来时,加载器会查看它是否与一个已经在进行中的请求相似。 如果是,则新请求被搁置,当原始请求得到响应时,该响应被复制到第二个请求。
重定向
有几种方法可以处理重定向请求。
默认情况下,URLSession 将遵循重定向,除非您特别覆盖 URLSessionTaskDelegate 上的 willPerformHTTPRedirection 委托方法。 因此,您可以这样做,然后有条件地允许根据您创建的特定 HTTPRequestOption 对请求进行重定向。
或者,您可以无条件地拒绝 URLSession 级别的重定向,然后有一个单独的 RedirectionFollowingLoader 接收传入请求,复制它们,并将重复项发送到链中。 当副本返回时,加载器检查响应并查看它是否是重定向响应。 如果是,那么它会为重定向构造一个新请求,并将其发回。
一旦加载程序返回非重定向响应,它就会使用该响应作为对原始请求的响应并将其发回。 您将需要一些逻辑来检测重定向循环并打破它们,但这里的关键思想是发送请求的副本,以便您有机会在决定如何处理之前检查响应。
证书固定
原则上,证书固定应该看起来像任何其他 HTTPLoader:一个请求进来,在它被发送到下一个请求之前,目标服务器的证书根据作为 HTTPRequestOption 附加到请求的证书进行验证。
实际上,这有点困难,因为证书仅在 URLSessionLoader 中协商到远程服务器的连接时可用。 因此,此处的操作过程是不使用单独的 CertificatePinningLoader,而是向 HTTPRequest 提供 CertificateValidator 值,如果加载程序需要执行某些证书验证(类似于 Alamofire 的 ServerTrustEvaluating 协议),则可以使用该值。
然后我们的 URLSessionLoader 需要更新为使用委托,并实现委托方法来处理 URLAuthenticationChallenge,然后在收到 .serverTrust 质询时为请求查询该选项。
点对点
点对点加载器很有趣,因为它源于这样一种认识,即 HTTPLoader 的合同没有说明响应来自哪个设备。 我们已经看到了加载程序的示例,这些加载程序将返回虚假响应(用于模拟)或重用响应(缓存和重复数据删除)。 P2P 加载器可以决定将请求发送到另一台设备,并允许该设备提供响应。
这可以通过多种技术来实现,从 MultipeerConnectivity 到蓝牙或直接套接字连接。 这里的可能性非常大。
敏锐的观察者还会意识到我们早先创建的 URLSessionLoader 属于此类。 这是一个加载程序,可以将生成请求的责任“卸载”到另一个设备。 它恰好是一个同时也是 HTTP 服务器的设备,但我们的加载堆栈并不需要直接知道这一点。
流式响应
该框架不能很好地工作的一个领域是流式响应。 这很明显:我们已经围绕这样的期望构建了一切,即离散且有限的请求具有离散且有限的响应。 流媒体打破了这种期望。 我们可以在上传中做一个流式主体,因为发送该流的过程是发送我们单个离散请求的一部分。
我们可以处理一些类型的流媒体主体,例如文件下载。 对于这些,我们想提供一个 OutputStream(或类似的)来表示“把你得到的任何字节放回这里”; 默认情况下,这可能是内存中数据值的流。 这将允许我们将响应直接流式传输到文件,而不是通过内存中的数据值。
对于实时视频流,我们可以提供一个 OutputStream 将数据通过管道传输到 AVSession 中。 但是,为了使这项工作有效,我们将明确放弃“单一请求,单一响应”的一些语义。 我们还需要非常小心我们如何实现请求复制(例如重定向加载程序需要)。
结论
我们可以用这个框架做很多事情。 有些事情有些复杂,需要解决/使用系统 API 的特定实现细节(例如证书固定、流式响应等)。 总的来说,这种将网络建模为“发送请求并最终获得响应”的方法使我们能够构建极其灵活、可组合和可定制的网络堆栈。
在下一篇(也可能是最后一篇)博文中,我们将缩小范围以查看我们创建的框架的高级概述,并了解它如何适应其他 Swift 技术。
Swift中的HTTP(十六)复合加载器
Swift中的HTTP(十五) 自动鉴权
Swift中的HTTP(十四) 自动鉴权设置
Swift中的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 进行身份验证的更复杂的场景。