对 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 进行身份验证的更复杂的场景。