版本记录
版本号 | 时间 |
---|---|
V1.0 | 2021.04.02 星期五 |
前言
Authentication Services
框架为用户提供了授权身份认证Authentication
服务,使用户更容易登录App
和服务。下面我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. Authentication Services框架详细解析 (一) —— 基本概览(一)
2. Authentication Services框架详细解析 (二) —— 使用Sign in with Apple实现用户身份验证(一)
3. Authentication Services框架详细解析 (三) —— 密码的自动填充(一)
4. Authentication Services框架详细解析 (四) —— 使用Account Authentication Modification Extension提升账号安全(一)
5. Authentication Services框架详细解析 (五) —— 使用web authentication session对App中的用户进行身份验证(一)
6. Authentication Services框架详细解析 (六) —— Web Browser App中支持Single Sign-On(一)
开始
首先看下主要内容:
了解什么是
OAuth
以及如何使用ASWebAuthenticationSession
实施它。内容来自翻译。
接着看下写作环境:
Swift 5, iOS 14, Xcode 12
接着,下面就是正文了。
您是否需要针对第三方应用程序对用户进行身份验证?也许您的客户要求您使用OAuth
标准实施这种机制?
如果您在企业环境中工作,并且客户端使用Active Directory
来管理用户,并使用Okta
或Ping Federate
来控制第三方应用程序与受保护资源的交互方式,该怎么办?
在本教程中,您将创建一个第三方GitHub
应用,您可以使用ASWebAuthenticationSession
通过OAuth
标准对第三方GitHub
应用进行身份验证,并显示用户拥有的存储库列表。在此过程中,您将了解:
OAuth
Session tokens
Ephemeral sessions
在针对服务器进行身份验证的传统客户端应用程序中,服务器存储用户名和密码以对用户进行身份验证并允许用户访问。当没有第三方参与时,这很好。
但是,当GitHub
之类的应用程序想要添加对第三方应用程序的支持时,会发生什么?
那是事情变得棘手的地方,并且存在一些缺点。没有OAuth
,可能会出现以下几个问题:
- 为了避免不断要求用户提供凭据,第三方应用必须在某个地方存储和管理它们。
- 密码身份验证方法更容易受到攻击。
-
GitHub
无法限制,撤销或限制对第三方应用程序的访问,因为它们具有用户的完整凭据。 - 如果第三方应用程序被盗用,那么用户的
GitHub
帐户也会被盗用。
GitHub
(服务器应用程序)如何为您(第三方应用程序)提供对其受保护资源的访问权,而又不赋予其完全,不受限制的访问权?这就是OAuth
的用武之地。
Understanding OAuth
在Internet Engineering Task Force’s (IETF)
网站上,这是OAuth 2.0
标准的定义:
OAuth 2.0
授权框架使第三方应用能够通过协调资源所有者和HTTP服务之间的批准交互,或者通过允许第三方应用来代表资源所有者获得对HTTP服务的有限访问权限,或者代表资源所有者代表自己获得访问权限。”
注意:
OAuth 1.0
标准已过时,已由本教程介绍的OAuth 2.0
取代。
您的第三方应用程序不会存储任何用户凭据,也不会直接处理对用户进行身份验证的操作。您希望拥有强大的基础架构和安全性的GitHub
为您服务。
您希望第三方应用程序执行的操作是允许用户使用自己的GitHub
凭据登录,以访问GitHub
中的特定资源。
另一种情况是使用GitHub进行用户身份验证 —— 不必花费太多时间在实现和安全细节上 —— 但在验证用户身份之外不访问任何GitHub
资源。
这就是为什么应用程序和网站经常使用带有按钮的登录功能来获取第三方服务(如Facebook
,Apple
或Google
)的原因。他们将这些事务留给那些各方来处理服务器安全性和登录基础结构。
1. Authorization Versus Authentication
OAuth
通过将身份验证过程(authentication process)
与授权过程(authorization process)
分开来提供帮助。
但是到底有什么区别呢?他们不一样吗?在多年交替使用这两个术语后,您可能会感到惊讶,但事实并非如此。
- Authentication - 身份验证:您是谁。
- Authorization - 授权:服务已授予您哪些权限。
在本教程中,GitHub
将为您的应用程序提供某些访问和许可:authorization
。但是,由用户来验证自己并验证他们是谁:authentication
。
2. OAuth Roles
到目前为止,您已经了解了什么是OAuth
以及身份验证和授权之间的区别。在幕后,OAuth
还有其他一些重要的概念需要学习。首先是角色roles。
OAuth
定义了四个角色:
- Resource Owner - 资源所有者:可以授予受保护资源访问权限的任何人。
- Resource Server - 资源服务器:包含受保护资源的服务器。
-
Client - 客户端:尝试以授权方式并代表
resource owner
访问受保护资源的应用程序。 -
Authorization Server - 授权服务器:成功进行身份验证和授权后,将
access token
提供给客户端。
3. The OAuth Flow
OAuth
标准将以下内容定义为第三方应用程序的典型流程。这是本教程中要实现的:
- 1) 您的应用程序要求
access token
,这是在针对resource owner
的请求中使用的短暂token
。为此,它必须向授权服务器(authorization server)
提供一个客户端ID,并允许用户使用其凭据进行身份验证。 - 2) 授权服务器对客户端进行身份验证,然后返回该用户的
access token
和refresh token
,仅在您的应用中使用。这是假设一切正常且一切顺利。 - 3) 您的应用程序(客户端)向
resource owner
发出对受保护资源的请求。然后,客户端必须出示用户的access token
,否则请求将失败。 - 4) 资源所有者会验证您用户的
access token
。如果有效,则返回请求的资源。 - 5) 您的应用将继续代表用户发出请求,直到
access token
过期。发生这种情况时,您对resource owner
的下一个请求将导致无效的token
错误。 - 6) 您的应用程序可以使用
refresh token
(通常是寿命较长的token
)来向授权服务器(authorization server)
请求新的access token
。适用范围和限制与以前相同。 - 7) 如果您的
refresh token
仍然有效,则authorization server
将为您的应用发布新的access token
。如果refresh token
已过期,则用户必须再次使用其凭据进行身份验证。
注意:还有其他验证方式,包括两个客户端之间的无浏览选项,或者您无权手动输入时。这些不在本教程的讨论范围之内。
OAuth
很大程度上是基于Web
的,这意味着大多数实现都向用户显示某种Web
视图,以使他们输入其凭据。在幕后,发生了一些重定向和回调,使所有工作正常进行。不用担心,本教程将帮助您处理所有这些。
到目前为止,已有很多理论,但是现在您对OAuth
及其工作原理有了更好的了解。
Creating a GitHub App
您希望您的应用程序与GitHub
对话。 GitHub
需要知道用户尝试从何处(您的应用程序)访问其资源,以及应该授予访问那哪些资源。
现在是时候通过您的帐户创建GitHub
应用了。 这是上手的方法!
在您的Web浏览器上,打开并登录GitHub
。 在右上角,单击您的profile image
。 然后,单击Settings
:
点击Click Developer settings
:
点击GitHub Apps
,点击New GitHub App
按钮:
创建新应用时,有很多选项和设置。 您现在将熟悉它们。
将您的应用命名为AuthHub-
。 为您的应用提供您选择的description
。
添加您应用的homepage URL
首页。 本示例使用https://www.raywenderlich.com/
。 如果您要创建自己的应用程序,则该应用程序应链接到您的应用程序主页,用户可以在其中获取更多信息。
1. User Authorization Settings
下一组设置围绕用户授权。 第一个选项是授权回调URL。 这是authentication provider
将在用户成功进行身份验证时将其重定向到的页面。 您在此处指定的格式是您的应用程序将收听的格式,以在用户进行身份验证后收回控制权。
将以下内容用作应用程序的回调URL:
authhub://oauth-callback
这只是您定义的回调。 您可以根据需要使用其他值,但是请注意,使用ASWebAuthenticationSession
时,它将更改设置过程。 目前,最简单的方法是使用上面的值,因此您可以继续进行下去。
下一个选项是一个复选框,询问是否让用户的授权tokens
过期。 启用此复选框是因为您想要获取refresh token
并选择使access token
过期以建立更好的安全模型。
最后,有一个选项可以在安装过程中请求用户授权,该选项可以保持未选中状态,因为您的应用程序不需要它:
2. Post Installation Options
向下移动页面,有post installation options and web hooks
,您可以将它们留为空白:
确保未选中Webhook
下的Active
复选框!
3. Permissions Settings
下一部分与权限有关。 您可以在此处指定您的应用应能够访问的信息。
在Contents
设置中,将访问选项更改为Read-only
:
最后,有一个用于安装应用程序的设置。 对于此示例,选择Only on this account
:
恭喜,您已配置了您的应用。 现在,点击Create GitHub App
,您将看到您的应用的详细信息页面。
4. Viewing Your Results
您已经在GitHub
中创建了一个具有自己的app ID
和client ID
的应用。 现在,您可以设置一个客户端(在本例中为AuthHub iOS
应用)来连接和使用您的GitHub
应用。 如果您有三个不同的应用程序(可能是iOS,Android
和Web
)连接到GitHub
应用程序,则需要生成三个不同的client secret
。
如果您在页面顶部看到一个黄色的横幅,告诉您必须生成一个private key
,请单击链接以执行此操作。
接下来,单击Generate a new client secret
按钮。 复制显示的字母数字值并将其粘贴到永久位置,因为GitHub
永远不会让您再次查看此信息。
请特别注意如何存储client secret
,并确保不共享它。
一切都在GitHub
端完成。 现在该写一些Swift
代码了!
Connecting GitHub App with ASWebAuthenticationSession
打开项目材料。 在Xcode
中打开启动项目。 构建并运行。
目前,轻按Sign In
不会执行任何操作。您需要在项目中实现登录功能。在此之前,您将探索入门项目。
您有一个带有两个屏幕的基本项目:一个用于登录,一个用于查看存储库。此外,您还具有用于User
,Repository
和NetworkRequest
的模型。
这里最有趣的模型是NetworkRequest
。它充当URLSession
的包装器。
打开NetworkRequest.swift
。您会发现一些枚举,这些枚举封装了网络层支持的HTTP
方法和错误。这里更有趣的枚举enum
是RequestType
,它列出了应用程序可以向GitHub
发出的受支持请求的案例。在枚举中,您还可以使用帮助程序方法为特定的请求类型构造NetworkRequest
。很方便!
有关更多信息,请查阅GitHub’s documentation on its REST API。
1. Updating the Project with GitHub App Values
为了让GitHub
知道您的应用程序,您需要将GitHub
应用程序的信息添加到项目中。
打开NetworkRequest.swift
。在// MARK: Private Constants
下,您会找到三个静态常量:
static let callbackURLScheme = "YOUR_CALLBACK_SCHEME_HERE"
static let clientID = "YOUR_CLIENT_ID_HERE"
static let clientSecret = "YOUR_CLIENT_SECRET_HERE"
将这些values
替换为新创建的GitHub
应用中的值。
回调URL scheme
不必是您在创建GitHub
应用程序时输入的完整URL,而只需是该scheme
。 对于此示例,将authhub
用作callbackURLScheme
的字符串。
继续,NetworkRequest
中的start(responseType:completionHandler :)
是实际的网络请求发出的地方。 如果您的应用程序具有access token
,则可以在此处为URL请求定义一些参数以及授权HTTP header
。
GitHub API希望您通过Authorization HTTP header
发送需要授权的请求的access token
。 此header
的值将采用以下格式:
Bearer YOUR_TOKEN_HERE
另外,该方法使用completion handler
处理所有error
,并将JSON
数据解析为参数中指定的原生Swift
类型。
到目前为止,一切都很好!
2. Views Overview
接下来,您将拥有视图。 这是一个相当简单的应用程序,因此仅需要两个视图:SignInView.swift
和RepositoriesView.swift
。 不必担心,有趣的事情发生在视图模型(view models)
中。
最后,您有了视图模型。
3. View Models Overview
打开RepositoriesViewModel.swift
。 您可以在此处找到代码,这些代码从GitHub
请求一个已登录用户的存储库列表,并将其提供给视图以显示在列表中。
应用程序中的另一个视图模型位于SignInViewModel.swift
中。 这是您要添加ASWebAuthenticationSession
的地方。 现在,您将进行处理。
4. Understanding ASWebAuthenticationSession
ASWebAuthenticationSession
是一种API
,它是Apple Authentication Services
框架的一部分,可用于通过web service
对用户进行身份验证。
您可以创建此类的实例,以获取开箱即用的解决方案,在该解决方案中,您可以将应用程序指向身份验证页面,允许用户进行身份验证,然后在应用程序中使用用户的authentication token
接收回调 。
这个API
的优点是它将适应其运行的原生平台。 对于iOS
,这意味着嵌入式安全浏览器,在macOS
上,您的默认浏览器(如果支持web authentication sessions
)或Safari
。
仅需几个参数,您就可以使用自己的(或第三方)身份验证服务启动并运行,而不必使用web view
从头开始实现所有功能。
5. Adding ASWebAuthenticationSession
ASWebAuthenticationSession
的工作方式是,它需要authentication URL
(带有来自authentication provider
的所有必需参数),应用程序的callback URL scheme
(以便在成功登录后返回到您的应用程序)和completion handler
,以供您执行以下操作: 处理和管理authentication token
。
打开SignInViewModel.swift
。 将以下代码添加到signInTapped()
:
guard let signInURL =
NetworkRequest.RequestType.signIn.networkRequest()?.url
else {
print("Could not create the sign in URL .")
return
}
您需要用于登录的URL
,因此您可以使用RequestType
来获取它。 如果由于某种原因该过程失败,则将error
打印到控制台,并且该方法将不执行任何操作而返回。
接下来,以相同的方法添加以下内容:
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
。 会话初始化程序期望sign-in URL and callback scheme
以及completion handler
作为参数。
回调URL scheme
是您刚刚在NetworkRequest
中替换的scheme
,但是sign-in URL
呢?
打开NetworkRequest.swift
。 查看url()
中的.signIn
情况。 在这里,您可以看到成功进行登录请求所需的host
,path
和parameters
。 值得注意的是client_id
,您在不久前将其添加到此文件中。
6. Checking for Errors
打开SignInViewModel.swift
,替换// Code will be added here next! :)
:
// 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
}
这是一个很大的guard statement
,但仍然是必要的。下面详细说明:
- 1) 您检查是否有错误,并确认有一个有效的回调
URL
。 - 2) 在此,您可以通过提取回调URL的
components
来获取URL的query
。query items
将帮助您检查此响应是否具有您需要交换tokens
的授权码。 - 3) 加载回调URL时,它包括授权代码作为
query parameter
。 - 4) 接下来,获取用于代码交换的
NetworkRequest
。 - 5) 如果这些检查中的任何一项失败,则将打印错误并从此方法返回。
构建并运行。
点击Sign In
。并检查结果……什么都没有?!
7. Setting the Presentation Context Provider
在authentication session
正常工作之前,您还需要做两件事。第一个是设置presentation context provider
。
为此,您首先需要实施必要的协议。现在,通过在SignInViewModel.swift
的末尾添加以下扩展来执行此操作:
extension SignInViewModel: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession)
-> ASPresentationAnchor {
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
return window ?? ASPresentationAnchor()
}
}
在这里,您实现ASWebAuthenticationPresentationContextProviding
来告诉您的authentication session
如何呈现自己。 在幕后,ASWebAuthenticationSession
与浏览器,Cookie
,会话等配合使用,向用户显示登录屏幕,然后将其重定向到您的应用程序。
现在,将以下内容添加到signInTapped()
的末尾:
authenticationSession.presentationContextProvider = self
您设置授权视图的位置。 这考虑了presentation portion
。
8. Starting the Authentication Session
在此之前,您需要做的第二件事是启动authentication session
。
在SignInViewModel
中,将以下代码添加到signInTapped()
中:
if !authenticationSession.start() {
print("Failed to start ASWebAuthenticationSession")
}
这将验证会话是否能够启动。如果不是,则另一个错误将打印到控制台。
构建并运行。
点击Sign In
按钮。如果您使用的是iOS 12.4
之前的iOS版本(这是authentication session API
的一部分,表明您的应用要使用GitHub
登录),则可能会看到alert
。
点击Continue
。
您会在GitHub
登录页面上看到一个modal controller
。成功登录后,模态将关闭,再次没有任何反应。
但这很好!不,是这样。
如果输入无效的凭据,则会在GitHub
页面本身中看到身份验证错误,但是如果模式已关闭且控制台中未显示任何错误,则会显示该错误。那是因为GitHub
用授权码回应了您的应用。
9. Handling the Authorization Code
此时,您需要交换authorization code
和refresh tokens
。
打开SignInViewModel.swift
。在signInTapped()
内部,在authenticationSession
的completion handler
的末尾添加以下代码:
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
按钮替换为活动视图。 这样可以防止您的用户在进行现有会话的过程中再次经历此流程。 您可以使用之前获取的网络请求作为guard statement
的一部分来执行token
交换。
NetworkRequest
的start(responseType:completionHandler :)
也有一个completion handler
。 在其中,您检查请求结果是否成功。 如果成功,则继续调用getUser()
。 如果失败,则将错误输出到控制台。
10. Displaying the Results
在再次运行该应用程序之前,将以下代码添加到getUser()
:
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
,告诉视图显示存储库。 否则,您将错误打印到控制台。
不管结果如何,您都可以告诉视图加载已完成。
构建并运行。 然后,像以前一样登录。
发生两个意外事件:
- 1) 模态只是显示和关闭而没有给您输入
GitHub credentials
的机会 - 2)
Xcode
控制台中将显示一条错误消息。
由于ASWebAuthenticationSession
在后台处理web views
,Cookie
和Web
会话,因此它已缓存了一个状态,该状态导致自动获取授权码。请注意,这发生的时间不确定,不是永远的,但仍然不是您想要的。
您稍后将在Creating Ephemeral Sessions部分中进行处理。现在,将重点放在有关token
交换期间失败的错误上。
在解决它之前,您需要对token
本身有一些理论上的了解。
Understanding Tokens
登录后,用户现在可以获取authorization code
。但是,这不是最终的停止点。此access code
的持续时间很短,通常只能使用一次。其目的是让您将其交换为access token
和refresh token
。
如前所述,access token
是随每个请求一起发送以授权您的请求的token
。refresh token
通常寿命更长。您可以使用它在过期时获取新的access token
。
Handling Tokens
有了authorization code
,就可以将其换成tokens
。 您还将在您的应用中添加逻辑以正确处理它们。
打开NetworkRequest.swift
。
在start(responseType:completionHandler :)
内部,并在以下代码块的正下方:
guard
error == nil,
let data = data
else {
DispatchQueue.main.async {
let error = error ?? NetworkRequest.RequestError.otherError
completionHandler(.failure(error))
}
return
}
找到这一行:
if let object = try? JSONDecoder().decode(responseType, from: data) {
将其替换为下面这些:
// 1
if T.self == String.self,
let responseString = String(data: data, encoding: .utf8) {
// 2
let components = responseString.components(separatedBy: "&")
var dictionary: [String: String] = [:]
// 3
for component in components {
let itemComponents = component.components(separatedBy: "=")
if let key = itemComponents.first,
let value = itemComponents.last {
dictionary[key] = value
}
}
// 4
DispatchQueue.main.async {
// 5
NetworkRequest.accessToken = dictionary["access_token"]
NetworkRequest.refreshToken = dictionary["refresh_token"]
completionHandler(.success((response, "Success" as! T)))
}
return
} else if let object = try? JSONDecoder().decode(T.self, from: data) {
其他GitHub API
响应是JSON
格式的,而token
交换响应则以字符串形式返回。处理这种情况的方法如下:
- 1) 通过添加此语句,您首先可以查看是否有字符串响应作为其内容。
- 2) 这个带有您的
tokens
的响应字符串是由与符号分隔的键值对组成的,这就是为什么您将该字符串分成键值对数组的原因。 - 3) 您遍历每个
components
以获取响应的Swift
字典。 - 4) 目前,由于
URLSession
的默认线程模型,一切都在后台线程中运行。因此,您调用DispatchQueue
来调用主线程上的下一个代码。您需要这样做,因为completion handler
将更新UI,这只能在主线程上完成。 - 5) 块中的代码在
NetworkRequest
的两个帮助器属性中存储access and refresh token
。然后,它继续调用completion handler
,指示该过程成功。
1. How NetworkRequest Handles the Tokens
要查看NetworkRequest
使用tokens
的功能,请切换到NetworkRequest + User.swift
。
这些是String
类型的属性,但是在后台,它们读取token
并将token
写入UserDefaults
,因此它们可以在应用程序启动期间持续存在。
这些token
是敏感数据,因此在您的应用中,您需要确保将它们安全地存储在钥匙串中。这是一个有趣的主题,您可以在 How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI中了解更多信息。
构建并运行。登录。虽然该模式可能会再次在屏幕上闪烁,但现在您将在此后看到存储库列表:
非常好!
Creating Ephemeral Sessions
最后一个要讨论的主题是临时会话(ephemeral sessions)
,这是私有身份验证会话。 临时会话不会将与会话相关的数据缓存到磁盘,而是缓存到RAM
。 会话无效后,临时会话的数据将清除会话数据。 这样可以方便地为用户提供更多的隐私和安全性。
打开。 SignInViewModel.swift
。 在signInTapped
及以下:
authenticationSession.presentationContextProvider = self
加下列代码:
authenticationSession.prefersEphemeralWebBrowserSession = true
这样可以缓解之前遇到的问题,即在第二次或第三次运行您的应用程序后立即尝试登录时,不会提示您输入用户凭据。 临时会话意味着ASWebAuthenticationSession
将不会缓存任何内容,并且始终会在会话开始时向用户询问其凭据。
构建并运行。
由于该应用将保留access and refresh tokens
,因此您仍将登录。 点击顶部的Sign Out
以清除token
。 现在,尝试登录后,系统会要求您输入GitHub
用户名和密码。 该应用程序不会缓存您之前的登录信息,因为这是私密会话(private session)
。
做得好! 在本教程中,您对OAuth
进行了了解,并使用ASWebAuthenticationSession
创建了自己的第三方GitHub
应用程序进行身份验证。 您可以尝试使用的一些其他功能通过将token
放入钥匙串并添加更多GitHub API
请求来增强token
的存储方式。
如果您想了解有关OAuth
的更多信息,请访问IETF’s page,其中包含整个标准。 有点多和密集,但很好的参考。
有关ASWebAuthenticationSession
的详细信息和文档,请访问Apple Developer Documentation。
有关网络的综合视频课程,请查看 Networking with URLSession。
后记
本篇主要讲述了使用
ASWebAuthenticationSession
实现OAuth
,感兴趣的给个赞或者关注~~~
网友评论