HTTP简介

HTTP根底结构

HTTP恳求体

HTTP 加载恳求

HTTP 模仿测验

HTTP 链式加载器

HTTP 动态修正恳求

HTTP 恳求选项

HTTP 重置

HTTP 吊销

HTTP 限流

HTTP 重试

HTTP 根底鉴权

HTTP 主动鉴权设置

HTTP 主动鉴权

HTTP 复合加载器

HTTP 脑筋风暴

HTTP 总结

尽管根本拜访身份验证适用于“根本”状况,但现在更常见的是运用某种办法的 OAuth。 与 Basic 身份验证比较,OAuth 有一些风趣的优势,例如:

该应用永久无法拜访用户的用户名和暗码 用户能够在不影呼应用程序拜访的状况下更改用户名或暗码 用户能够远程吊销应用程序的拜访权限 这些优势是以添加复杂性为价值的,而这正是咱们将在本文中讨论的复杂性。

主动鉴权流程

根本(无错误)OAuth 流程如下所示:

Swift中的HTTP(十四)  自动鉴权设置

当咱们决议开端身份验证时,咱们首要查看是否有任何已保存的令牌。 这儿有三种或许的成果:

  • 咱们有凭证,而且还没有过期
  • 咱们有凭证,但它们已过期
  • 咱们没有凭证

榜首种状况很简略。 假如咱们有未过期的凭证,那么咱们不需求做任何工作。 第二种状况也很简略。 假如咱们有过期的凭证,咱们能够将它们发送到身份验证服务器以获取“新”版别的令牌(假定用户没有吊销拜访权限),然后咱们就完结了。

假如咱们没有任何凭证,则需求要求用户登录。这是通过构建登录网页的 URL,然后将该页面显现给用户来完结的。 用户登录网页,显现该页面的服务器验证凭证的正确性。 假定用户名和暗码正确,网页将重定向到一个新的 URL,浏览器会阻拦该 URL 并将其重定向到应用程序。

此重定向 URL 具有由服务器生成的特殊代码,应用程序可运用该代码获取拜访凭证。 因而,有了代码,应用程序现在回身问询服务器:“鉴于此代码,我需求授权令牌”。 服务器用令牌呼应,进程完结。

OAuth 流程界说十分清晰,是状况机的一个很好的比如。 有几件详细的工作要做,而且它们完结的次序是严格界说的,而且只允许该次序。

假如咱们要在 Swift 中完结这个流程,一种常见的办法或许是运用某种 State 枚举,就像咱们在 Basic Authentication 帖子中看到的那样。 可是,考虑到状况的数量和允许的十分清晰的流程,我认为有必要采用更正式的办法。

主动鉴权状况机

首要,咱们将界说一个代表整个进程的“状况机”类:

class OAuthStateMachine {
    func run() { }
}

接下来,咱们将界说一个代表上图中“圆”的 OAuthState 类,并为状况机供给一个状况。 该状况将有一个 enter() 办法,当咱们“进入”该状况时将调用该办法,它应该开端履行其逻辑:

class OAuthState {
    func enter() { }
}
class OAuthStateMachine {
    private var currentState: OAuthState!
}

一个状况需求一种办法来告知机器它何时准备好继续前进,所以它需求一个对机器的引用,以及一种移动状况的办法:

class OAuthState {
    unowned var machine: OAuthStateMachine!
    func enter() { }
}
class OAuthStateMachine {
    private var currentState: OAuthState!
    func move(to newState: OAuthState) {
        currentState?.machine = nil
        newState.machine = self
        currentState = newState
        currentState.enter()
    }
}

现在咱们能够界说对应于咱们的图表的状况:

class GetSavedCredentials: OAuthState { }
class LogIn: OAuthState { }
class GetTokens: OAuthState { }
class RefreshTokens: OAuthState { }
class Done: OAuthState { }

对于其间的每一个,咱们都需求完结它们的 enter() 办法。 让咱们看看每一个。

检索凭证

GetSavedCredentials 状况是当咱们需求回身并问询应用程序是否在钥匙串(或其他安全存储位置)中为咱们保存了任何凭证时。 在根本拜访帖子中,咱们通过托付完结了此操作。 咱们将在这儿采用相似的办法。

class GetSavedCredentials: OAuthState {
    override func enter() {
        // we need to ask someone if there are any save credentials
        // let's assume the state machine itself has a delegate we can ask
        let delegate = machine.delegate
        DispatchQueue.main.async {
            // it's always polite to invoke delegate methods on the main thread
            delegate.stateMachine(self.machine, wantsPersistedCredentials: { credentials in
                // this closure will be called with either the credentials that were saved, or "nil"
                self.processCredentials(credentials)
            })
        }
    }
    private func processCredentials(_ credentials: OAuthCredentials?) {
        let nextState: OAuthState
        if let credentials = credentials, credentials.expired == false {
            // we got credentials and they're not expired
            nextState = Done(credentials: credentials)
        } else if let credentials = credentials {
            // we got credentials but they are expired
            nextState = RefreshTokens(credentials: credentials)
        } else {
            // we did not get credentials
            nextState = LogIn()
        }
        machine.move(to: nextState)
    }
}

这真的便是它的全部。 当咱们“进入”状况时,咱们问询代理是否有任何已保存的凭证。 在某个时分它会返回给咱们,所以咱们查看成果并决议下一步去哪里。

改写Tokens

咱们从身份验证服务器取得的令牌包括两部分:“改写”令牌和“拜访”令牌。 拜访令牌是咱们用来对每个恳求进行身份验证的东西,它的生命周期往往很短。 它的有用期从几分钟到几天不等。

在某个时分,它会“过期”(这个过期日期包括在咱们作为令牌的一部分取得的数据中)。 产生这种状况时,咱们运用另一个令牌(“改写”令牌)向服务器恳求新的拜访令牌。 这是 OAuth 测验为用户供给尽或许多的控制权的办法之一。 当用户吊销对应用程序的拜访时,它不只会使拜访令牌失效,还会使改写令牌失效。 这意味着应用程序不能只获取新令牌并仍然保持拜访权限,而是彻底失去拜访权限。

RefreshTokens 状况运用此“改写令牌”来获取新凭证。 假定 OAuthStateMachine 有一个 HTTPLoader,咱们能够运用它来恳求新的凭证(例如,这或许是整个 OAuth 加载程序的 .nextLoader)。

class RefreshTokens: OAuthState {
    let credentials: OAuthCredentials
    override func enter() {
        var request = HTTPRequest()
        // TODO: construct the request to point to our OAuth server
        request.body = FormBody([
            URLQueryItem(name: "client_id", value: "my_apps_client_id"),
            URLQueryItem(name: "client_secret", value: "my_apps_client_secret"),
            URLQueryItem(name: "grant_type", value: "refresh_token"),
            URLQueryItem(name: "refresh_token", value: credentials.refreshToken)
        ])
        machine.loader.load(request: request, completion: { result in
            self.processResult(result)
        })
    }
    private func processResult(_ result: HTTPResult) {
        let nextState: OAuthState
        switch result {
            case .failure(let error):
                // TODO: do we give up here? Or maybe we could ask the user to log in?
                nextState = Done(credentials: nil)
            case .success(let response):
                // this could be any response, including a "401 Unauthorized" response
                if let credentials = OAuthCredentials(response: response) {
                    // TODO: notify the delegate that we have new credentials to save
                    nextState = Done(credentials: credentials)
                } else {
                    // TODO: do we give up here? Or maybe we could ask the user to log in?
                    nextState = Done(credentials: nil)
                }
        }
        machine.move(to: nextState)
    }
}

鉴于咱们现有的 OAuthCredentials,咱们运用其间的 .refreshToken 向服务器恳求新的拜访令牌。 假如咱们得到它,咱们能够告知托付人保存它并移动到“完结”状况。 假如出现问题,那么咱们能够抛弃(在没有凭证的状况下转到“完结”),或许咱们能够直接进入“登录”状况并要求用户再次登录。 这个特定的挑选是咱们的,做出这个改动是实例化一个 LogIn 实例而不是 Done 实例的问题。

登录

假如咱们未能取得有用的拜访令牌(无论是没有保存仍是无法理解呼应),咱们或许需求要求用户登录。此状况将与以下相同简略 GetSavedCredentials 状况:

class LogIn: OAuthState {
    let state = UUID()
    override func enter() {
        var loginURL = URLComponents()
        // construct a URL according to the specification for the server. This will likely be something like this:
        loginURL.scheme = "https"
        loginURL.host = "example.com"
        loginURL.path = "/oauth/login"
        loginURL.queryItems = [
            URLQueryItem(name: "client_id", value: "my_apps_client_id"),
            URLQueryItem(name: "response_type", value: "code"),
            URLQueryItem(name: "scope", value: "space separated list of permissions I want"),
            URLQueryItem(name: "state", value: state.uuidString)
        ]
        let url = loginURL.url! // this should always succeed
        // TODO: what if in some alternate reality this fails. Then what?
        DispatchQueue.main.async {
            let delegate = self.machine.delegate
            delegate.stateMachine(self.machine, displayLoginURL: url, completion: { callbackURL in
                self.processCallbackURL(callbackURL)
            })
        }
    }
    private func processCallbackURL(_ url: URL?) {
        let nextState: OAuthState
        // TODO: if url is nil, then the user cancelled the login process → Done(credentials: nil)
        // TODO: if we got a url but its "state" query item doesn't match self.state, the app called the wrong callback → Done(credentials: nil)
        // TODO: if we see a "code" query item in the URL → GetTokens(code: code)
        machine.move(to: nextState)
    }
}

这在概念上是一个十分简略的状况。 与 GetSavedCredentials 相同,这是一种咱们“等候应用程序做某事,当它完结后会告知咱们”的状况。

“应用程序做某事”的那部分能够是几件不同的工作。 这个状况所做的便是给应用程序一个 URL,应用程序需求以某种办法运用它来让用户登录。这能够通过 WKWebView(不引荐),弹出到 Safari(或许会破坏),或许运用 ASWebAuthenticationSession(或许是最好的体会)。

显现 WKWebView 很简略,但它会让用户面对风险,因为应用程序或许会看到用户在这样的 Web 视图中输入的内容。 因而,您不应该将这些用于敏感场景,例如登录。假如您挑选运用其间之一(您不应该这样做),您将运用 Web 视图的 WKNavigationDelegate 来查看用户何时完结以及服务器何时测验 将流程重定向回应用程序。 您将阻拦重定向的 URL,并运用该 URL 调用供给给状况机托付办法的回调。 当然,假如用户决议吊销,你会用 nil 调用回调。 可是,不要运用这种办法。

假如您决议将用户弹出到 Safari,您将运用 UIApplication(或 NSWorkspace)翻开 URL。 该应用程序还需求将回调保存在某处。 在 Safari 中,服务器将重定向到应用程序注册处理的 URL(通过其 Info.plist),此时应用程序将再次激活,您的 application(_:open:options:) 托付办法将是 调用,然后将 URL 传回回调。 当然,运用这种办法,您无法知道用户是否已吊销。

最好的办法是运用 ASWebAuthenticationSession 在您的应用程序中显现安全浏览器,同时在会话结束时供给回调。 例如,假如您的应用程序中有一个 UIWindowSceneDelegate,您能够将其用作出现上下文:

extension MySceneDelegate: ASWebAuthenticationPresentationContextProviding {
    // this is the method that would get called by way of the state machine delegate method
    func displayLoginSession(_ url: URL, completion: @escaping (URL?) -> Void) {
        self.authSession = ASWebAuthenticationSession(url: url, callbackURLScheme: Bundle.main.bundleIdentifier!, completionHandler: { [weak self] url, error in
            self?.authSession = nil
            completion(url)
        })
        self.authSession?.prefersEphemeralWebBrowserSession = true
        self.authSession?.presentationContextProvider = self
        self.authSession?.start()
    }
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        // which window are we presenting the session in? the scene's window!
        return window!
    }
}

因为咱们现已将“登录所需的 UI”与“整个身份验证进程”分离,咱们实际上终究在整个体会中取得了相当大的灵活性,而且能够让应用程序开发者为他们的应用程序挑选最佳体会,而无需 咱们(作为图书馆作者)必须为他们做出决议。

获取Tokens

我不会在这儿列出完整的状况,但这在概念上与 RefreshTokens 状况相同。 在这种状况下,咱们将收到的代码作为登录回调 URL 的一部分,并将其与其他所需的位(例如咱们的客户端 ID 和客户端暗码)一同发送到服务器。 假定一切都查看完毕,咱们将取回一个不错的新改写令牌和有用拜访令牌,咱们能够要求应用程序为咱们保存它们,然后转到咱们终究的“完结”状况。

Done

正如咱们到目前为止所见,能够运用一组有用的凭证或 nil(表示出现问题)调用 Done 状况。 当咱们进入这个状况时,它需求以某种办法向机器宣布信号表明整个进程现已完结(或许是 OAuthStateMachine 上的另一个新办法)而且机器能够获取 Done 状况取得的任何值并将其返回给 调用状况机的库。

两大坑

我从这个状况机中遗失了两个显着的遗失。

我遗失的榜首件事,就像我在所有完结中遗失的相同,是线程安全的概念。 我把它排除在外是因为它是相当多的样板代码,我的方针是这些帖子的可读性,而不是“彻底正确”。

另一件事是由问题提示的:假如咱们在这个状况机的中心而且咱们被要求重置()整个加载链会产生什么? 为了习惯这一点,每个状况或许还需求有一个 reset() 办法,它能够用来履行任何整理(例如吊销网络恳求),然后立即转移到新的 LogOut 状况。 LogOut 状况将负责告知托付人保存一组新的凭证(nil,意思是“删除你拥有的”),或许会显现注销网页,使服务器的凭证过期等等。 为简洁起见,我将其省掉,但假如您终究实施 OAuth,则正确的做法是考虑这种状况。

总结

这是咱们为咱们的库完结 OAuth 加载程序所需的全体设置。 鄙人一篇文章中,咱们将运用此 OAuthStateMachine 主动获取和改写令牌以用于通过身份验证的恳求。