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

5,809 阅读4分钟

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