原文链接:Implementing OAuth with ASWebAuthenticationSession | Kodeco
iOS 用 ASWebAuthenticationSession 实现 OAuth 登录
使用 ASWebAuthenticationSession 连接 GitHub App
点击该指南顶部或底部的 Download Materials 按钮下载项目资源。在 XCode 中打开 starter project ,编译运行。
现在,点击 Sign In 不会做任何处理。你需要在工程中实现登录特性。 在做这些之前,可以先浏览一下 starter 项目。
现在是有一个基础的项目,它有两个画面:一个用于登录、一个用于查看代码仓库。然后有数据模型 User
、 Repository
和 NetworkRequest
。
这里最有趣的数据模型是 NetworkRequest
。它作为 URLSession
的封装使用。
打开 NetworkRequest.swift 。你会发现枚举为网络层概括了支持的 HTTP 方法 和错误。 这里更有趣的 enum
是 RequestType
, 它列出了能够支持的 APP 向 GitHub 发起的请求。 在枚举中,还有辅助方法为指定的请求类型构建 NetworkRequest
(网络请求)。 非常顺手!
更多信息查看 GitHub's documentation on its REST API。
用 GitHub APP 的设置值升级项目
要想让 GitHub 知道你的 APP ,需要将 GitHub APP 信息加到项目中。
打开 NetworkRequest.swift ,在 // MARK: Private Constants
下面,你会找到三个静态常量:
csharp
static let callbackURLScheme = "YOUR_CALLBACK_SCHEME_HERE"
static let clientID = "YOUR_CLIENT_ID_HERE"
static let clientSecret = "YOUR_CLIENT_SECRET_HERE"
将这些值替换为你刚刚创建的 GitHub APP 的值。
回调的 URL Scheme 并不需要是在创建 GitHub APP 时输入的完整 URL ,它只需要是 scheme 。 就本例来说,使用 authhub 作为 callbackURLScheme
(回调Scheme)。
然后继续,NetworkRequest
中的 start(responseType:completionHandler:)
是实际发出网络请求的地方。 在这里,你需要为带有授权 HTTP 头部的 URL 请求定义一些参数,你的 APP 应该有可用的访问令牌。
GitHub API 会期望你通过Authorization HTTP 头部为需要授权的请求发送访问令牌。该头部的值会是下面的格式:
Bearer YOUR_TOKEN_HERE
此外,该方法会使用完成处理程序处理任何错误,并将JSON数据解析为参数中指定的原生Swift类型。
至今为止,很棒!
视图概览
接下来,就是视图。这是一个非常简单的 APP,所以只有两个必要的视图:SignInView.swift 和 RepositoriesView.swift 。不需要过于担心,有趣的内容在视图模型里。
最后是视图模型。
视图模型概览
打开 RepositoriesViewModel.swift。在这里你会找到请求已登录 GitHub 用户的代码仓库列表和提供视图以显示列表的代码。
APP 中的另外一个视图模型在 SignInViewModel.swift 中。在这里你会添加你的 ASWebAuthenticationSession
,马上就会用到它。
理解 ASWebAuthenticationSession
ASWebAuthenticationSession
是 Apple 认证服务 框架 的一个API ,可用来通过 Web 服务认证用户。
你创建了该类的一个实例,可用于在 App 的认证页面利用开箱即用的方案。它允许用户进行认证,然后在应用中接收一个带用户认证令牌的回调方法。
该 API 很酷的是它会适配所运行的原生平台。 对于 iOS ,这意味着嵌入式的安全浏览器;在 MacOS 上,是默认浏览器(如果该浏览器支持 Web 认证 Session)或 Safari 。
只需要很少的参数,就可以马上运行你的(或第三方)认证服务,而不需要用Web视图进行临时处理来实现。
添加 ASWebAuthenticationSession
ASWebAuthenticationSession
的工作方式是它会期望一个认证 URL (带有认证提供方所需要的所有参数),你的 App 的回调 URL Scheme (为了登录成功时返回),和用于操作和管理认证令牌的完成处理器。
打开 SignInViewModel.swift 。添加代码到 signInTapped()
中:
swift
guard let signInURL =
NetworkRequest.RequestType.signIn.networkRequest()?.url
else {
print("Could not create the sign in URL .")
return
}
你需要用来登录的 URL ,所以你使用 RequestType
来获取它。 如果因为某些原因处理失败了,控制台会打印出错误,该方法不会做任何事情并返回。
接下来,在相同的方法中,添加以下内容:
swift
let callbackURLScheme = NetworkRequest.callbackURLScheme
let authenticationSession = ASWebAuthenticationSession(
url: signInURL,
callbackURLScheme: callbackURLScheme) { [weak self] callbackURL, error in
// Code will be added here next! :)
}
这里,你首先创建一个常量来存储你的 回调 URL scheme ,然后继续创建一个新的 ASWebAuthenticationSession
。 Session初始化器需要登录URL和回调 scheme 作为完成后处理的参数。
The callback URL scheme is the one you just replaced inside NetworkRequest
, but what about the sign-in URL?
回调 URL scheme 是你刚刚在 NetworkRequest
中替换的,但是什么是 登录 URL ?
打开 NetworkRequest.swift 。看一下 url()
的 .signIn
处理。 在这里,你能看到创建成功的登录请求所需的 主机、路径和参数。 值得注意的是 client_id
,之后需要你添加到该文件中。
检查错误
打开 SignInViewModel.swift , 用下面的内容替换 // Code will be added here next! :)
:
swift
// 1
guard
error == nil,
let callbackURL = callbackURL,
// 2
let queryItems = URLComponents(string: callbackURL.absoluteString)?
.queryItems,
// 3
let code = queryItems.first(where: { $0.name == "code" })?.value,
// 4
let networkRequest =
NetworkRequest.RequestType.codeExchange(code: code).networkRequest()
else {
// 5
print("An error occurred when attempting to sign in.")
return
}
这是一个相当有分量的守护声明,但仍然是必要的。 事情是这样的:
- 检查错误然后确认这是一个有效的回调 URL 。
- 从这里,通过抽取回调 URL 中的组成部分获取到 URL 的查询项目。 查询项目会帮助你检查响应是否带有你需要用令牌换取的授权码。
- 当回调 URL 加载时,它包含作为查询参数的授权码。
- 接下来,获得一个授权码兑换的
NetworkRequest
。 - 如果其中某个检查失败了,你会打印错误并从方法返回。
编译运行。
点击 Sign In 按钮。然后确认结果... 没反应?!
设置 Presentation Context Provider
在认证 Session 生效之后,还有两件事需要做。 第一个就是设置一个 presentation context provider (展示上下文提供者)。
首先要实现必须的协议。 现在需要在 SignInViewModel.swift 的末尾添加下面的扩展。
swift
extension SignInViewModel: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession)
-> ASPresentationAnchor {
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
return window ?? ASPresentationAnchor()
}
}
这里,你实现了 ASWebAuthenticationPresentationContextProviding
来告诉你的权限 Session 如果呈现自身。 在该场景中,ASWebAuthenticationSession
会使用浏览器、Cookie、SESSION和其它展示给用户一个登录页面,之后会重定向到你的 APP 。
现在在 signInTapped()
的末尾添加以下代码:
swift
authenticationSession.presentationContextProvider = self
为授权视图设定位置。这负责展示内容。
启用权限 SESSION
第二件要做的事是启用权限SESSION。
在 SignInViewModel
的 signInTapped()
里添加以下代码:
swift
if !authenticationSession.start() {
print("Failed to start ASWebAuthenticationSession")
}
这会验证 SESSION 是否能够开始。如果不能的话,会向控制台打印另外的错误。
编译运行。
点击 Sign In 按钮,如果是在 12.4 之前的 iOS 上运行,你会看到一个警告信息(这是认证SESSION API 的一部分,会指明你的 App 要使用 GitHub 来登录)。
点击 Continue 。
你会看到 GitHub 登录页面的一个模态控制器。成功登录之后,模态控制器会消失,再来一次则不会有任何响应。
但这已经很棒了!是的,真的,已经很棒了。
如果你输入了非法凭证,你会看到 GitHub 页面自身的认证错误。 如果模态控制器消失了,然后控制台没有打印任何错误,那是因为 GitHub 已经向你的 App 响应了授权码。
处理授权码
这一点上,你需要为访问令牌和刷新令牌交换授权码。
打开 SignInViewModel.swift 。在 signInTapped()
里 authenticationSession
的完成处理器中,添加以下代码:
swift
self?.isLoading = true
networkRequest.start(responseType: String.self) { result in
switch result {
case .success:
self?.getUser()
case .failure(let error):
print("Failed to exchange access code for tokens: (error)")
}
}
该部分执行后,会告诉视图正在加载一些内容,这会用一个活动视图替换掉 Sign In 按钮。 这会阻止用户在处理现有 SESSION 的过程中直接越过整个流程。 你使用已获取的网络请求作为守护语句的一部分来实行令牌交换。
NetworkRequest
的 start(responseType:completionHandler:)
也有一个完成处理器。 在这里可以检查请求结果成功或失败。 如果成功了,会调用 getUser()
。如果失败了,会向控制台打印错误。
显示结果
再次运行 APP 之前,向 getUser()
添加以下代码:
swift
isLoading = true
NetworkRequest
.RequestType
.getUser
.networkRequest()?
.start(responseType: User.self) { [weak self] result in
switch result {
case .success:
self?.isShowingRepositoriesView = true
case .failure(let error):
print("Failed to get user, or there is no valid/active session: (error)")
}
self?.isLoading = false
}
该方法和使用 NetworkRequest
所做的类似,预期这会获得已登录用户的信息。 如果请求成功了,你会设置一个布尔值为 true
,以告诉视图显示仓库。 否则,会向控制台打印一条错误信息。
先不管结果,你已经告诉视图,加载已经结束。
编译运行。然后,登录会和之前一样。
会有两件预期外的事情发生:
- 模态窗口显示并结束,不会给你输入 Github 凭证的机会。
- XCode的控制台会显示一条错误信息。
因为在该场景的背后, ASWebAuthenticationSession
使用WEB视图、cookie 和 WEB session,它已经自动缓存了授权码。注意,这只会持续一段不确定的时间,并不是一直持续,但这仍然是你所期望的。
你可以之后在 创建短期 SESSION 部分之后进行处理。现在先集中解决令牌交换时失败的错误。
在你能解决之前,你需要了解一些令牌本身的理论。