OAuth2
Swift 5.0编写的针对 macOS、iOS 和 tvOS 的 OAuth2 框架。
⤵️ 安装🛠 使用方法🖥 示例 macOS 应用(包含数据加载器示例)📖 技术文档
OAuth2 需要 Xcode 10.2,生成的框架可以在 OS X 10.11 或 iOS 8 及更高版本上使用。欢迎使用贡献,请参阅 CONTRIBUTING.md
Swift 版本
由于 Swift 语言不断进化,我采用了与 Swift 版本相对应的版本控制方案:框架版本的前两位数字总是与库兼容的 Swift 版本,请参阅 releases。与最新 Swift 版本兼容的代码在名为适当分支的单独功能分支上可以找到。
使用方法
要在您的自代码中使用 OAuth2,请在源文件中从 import OAuth2
开始。
在OAuth2中存在多种不同的流程。本库支持所有这些流程,请确保您正在使用适合您用例和授权服务器的正确流程。下面的典型代码授权流程用作演示。其他流程的步骤大致相同,只需实例化不同的子类并使用不同的客户端设置即可。
仍然不起作用吗?请参阅特定站点特性。
1. 使用设置字典实例化OAuth2
在本例中,您将构建一个访问GitHub的iOS客户端,因此以下代码将在您的某个视图控制器中,例如应用委托。
let oauth2 = OAuth2CodeGrant(settings: [
"client_id": "my_swift_app",
"client_secret": "C7447242",
"authorize_uri": "https://github.com/login/oauth/authorize",
"token_uri": "https://github.com/login/oauth/access_token", // code grant only
"redirect_uris": ["myapp://oauth/callback"], // register your own "myapp" scheme in Info.plist
"scope": "user repo:status",
"secret_in_body": true, // Github needs this
"keychain": false, // if you DON'T want keychain integration
] as OAuth2JSON)
看到那些redirect_uris
了吗?您可以使用想要的任何方案,但是您必须在您的Info.plist
中声明您使用的方案,并且必须在您所连接的授权服务器上注册相同的URI。
请注意,自iOS 9开始,您应该使用通用链接作为您的重定向URL,而不是自定义应用方案。这可以防止其他人重用您的URI方案并截获授权流程。
如果您针对的是iOS 12及其以上版本,您应该使用ASWebAuthenticationSession
,它使使用您的本地重定向方案变得安全。
想避免切换到Safari并弹出SafariViewController或NSPanel吗?设置此选项
oauth2.authConfig.authorizeEmbedded = true
oauth2.authConfig.authorizeContext = <# your UIViewController / NSWindow #>
需要指定一个单独的刷新令牌URI吗?您可以在设置字典中设置refresh_uri
。如果指定了,则库将使用您指定的refresh_uri
刷新访问令牌,否则它将使用token_uri
。
需要调试吗?使用.debug
或甚至.trace
日志记录器。
oauth2.logger = OAuth2DebugLogger(.trace)
有关更多信息,请参阅下方的高级设置。
2. 让数据加载器或Alamofire接管
从版本3.0开始,有一个OAuth2DataLoader
类,您可以使用它从API获取数据。如果需要,它将自动启动授权,并确保即使您有多个调用正在进行时也能正常工作。有关配置授权的详细信息,请参见以下第4步,在本例中,我们将使用“嵌入式”授权,这意味着如果用户需要登录,我们将在iOS上显示SFSafariViewController。
此维基页面包含了您需要的内容,以便轻松使用OAuth2和Alamofire。
let base = URL(string: "https://api.github.com")!
let url = base.appendingPathComponent("user")
var req = oauth2.request(forURL: url)
req.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
self.loader = OAuth2DataLoader(oauth2: oauth2)
loader.perform(request: req) { response in
do {
let dict = try response.responseJSON()
DispatchQueue.main.async {
// you have received `dict` JSON data!
}
}
catch let error {
DispatchQueue.main.async {
// an error occurred
}
}
}
3. 确保拦截回调
当使用 OS 浏览器或 iOS 9+ Safari 视图控制器时,您需要在您的应用程序代理中拦截回调并允许 OAuth2 实例处理完整的 URL。
func application(_ app: UIApplication,
open url: URL,
options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
// you should probably first check if this is the callback being opened
if <# check #> {
// if your oauth2 instance lives somewhere else, adapt accordingly
oauth2.handleRedirectURL(url)
}
}
对于 iOS 13,在 SceneDelegate.swift
中设置回调。
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
AppDelegate.shared.oauth2?.handleRedirectURL(url)
}
}
配置完成!
如果您想深入了解或自行进行授权,请参考以下内容
4. 手动授权用户
默认情况下,如果不存在访问令牌或在密钥链中,OS 浏览器将被用于授权。从 iOS 12 开始,当在 iOS 上启用嵌入式授权时,将使用 ASWebAuthenticationSession
(以前,从 iOS 9 开始,使用的是 SFSafariViewController
)。
要开始授权,请调用 authorize(params:callback:)
或使用便捷方法 authorizeEmbedded(from:callback:)
来启用嵌入式授权。
登录屏幕仅在必要时才会呈现(有关详细信息,请参阅下文中的 _Manually Performing Authorization_),并在成功后自动 关闭 登录屏幕。有关其他选项,请参阅 高级设置。
oauth2.authorize() { authParameters, error in
if let params = authParameters {
print("Authorized! Access token is in `oauth2.accessToken`")
print("Authorized! Additional parameters: \(params)")
}
else {
print("Authorization was canceled or went wrong: \(error)") // error will not be nil
}
}
// for embedded authorization you can simply use:
oauth2.authorizeEmbedded(from: <# presenting view controller / window #>) { ... }
// which is equivalent to:
oauth2.authConfig.authorizeEmbedded = true
oauth2.authConfig.authorizeContext = <# presenting view controller / window #>
oauth2.authorize() { ... }
不要忘记,当使用 OS 浏览器或 iOS 9+ Safari 视图控制器时,您需要在应用程序代理中拦截回调。如上步 2 中所示。
有关如何在 Mac 上执行此操作的详细信息,请参阅下文中的 手动执行授权。
5. 处理回调
完成所有步骤后,回调将被调用,无论是带有非 nil 的 authParameters 字典(可能为空)还是一个错误。访问令牌和刷新令牌及其到期日期已提取出来,并作为 oauth2.accessToken
和 oauth2.refreshToken
参数提供。如果您想提取其他信息,则只需要检查 authParameters 字典。
对于以下详细说明的高级使用,有 afterAuthorizeOrFail
块可以用于您的 OAuth2 实例。如其名称所示,internalAfterAuthorizeOrFail
闭包是内部提供的 – 它是为子类化和编译目的而公开的,您不应触碰它。自版本 3.0.2 以来,您不能再使用 onAuthorize
和 onFailure
回调属性,它们已经被完全移除。
6. 发送请求
现在你可以获取一个 OAuth2Request
,这是一个已经签名的 MutableURLRequest
,用于从你的服务器获取数据。此请求使用以下方式设置 Authorization 头部,使用访问令牌:Authorization: Bearer {你的访问令牌}
。
let req = oauth2.request(forURL: <# resource URL #>)
// set up your request, e.g. `req.HTTPMethod = "POST"`
let task = oauth2.session.dataTaskWithRequest(req) { data, response, error in
if let error = error {
// something went wrong, check the error
}
else {
// check the response and the data
// you have just received data with an OAuth2-signed request!
}
}
task.resume()
当然,你可以使用自己的 URLSession
进行这些请求,不必使用 oauth2.session
;使用如在步骤2中所示的OAuth2DataLoader,或将它传给 Alamofire。关于如何在Alamofire中轻松使用OAuth2,请看这里。
7. 取消授权
您可以通过调用 oauth2.abortAuthorization()
在任何时间取消正在进行的授权。这将取消正在进行的请求(如代码交换请求)或在您等待用户在网页上登录时调用回调。后者将关闭嵌入的登录界面或将用户重定向回应用程序。
8. 重新授权
在执行请求之前始终调用 oauth2.authorize()
是安全的。您也可以在您的应用程序再次激活后的第一个请求之前执行授权。或者,您可以在请求中拦截401并在此尝试请求之前再次调用授权。
9. 登出
如果您将令牌存储在密钥链中,可以通过调用 forgetTokens()
来删除它们。
但是,您的用户可能仍然登录到网站上,因此在进行下一次 authorize()
调用时,网页视图可能立即出现然后消失。当使用iOS 8的内置网页视图时,可以在以下代码段中抛出任何应用程序创建的cookies。使用较新的 SFSafariViewController
或在浏览器中执行登录时,最好是直接 打开退出页面 以使用户看到退出发生。
let storage = HTTPCookieStorage.shared
storage.cookies?.forEach() { storage.deleteCookie($0) }
手动执行授权
方法 authorize(params:callback:)
将执行以下操作:
- 检查是否已经有授权调用正在进行,如果有,将使用
OAuth2Error.alreadyAuthorizing
错误中止 - 检查是否存在未过期的访问令牌(或已在密钥链中),如果不存在
- 检查是否存在刷新令牌,如果找到
- 尝试使用刷新令牌获取新的访问令牌,如果失败
- 通过使用
authConfig
设置启动 OAuth2 舞步,以确定如何向用户显示授权屏幕
您的 oauth2
实例将使用通过 ephemeralSessionConfiguration()
配置自动创建的 URLSession
对其请求进行请求,并在 oauth2.session
中公开。您可以设置 oauth2.sessionConfiguration
为您自己的配置,例如,如果您想更改超时值。您还可以设置 oauth2.sessionDelegate
为您自己的会话代理。
维基上有 授权()方法的完整调用图。如果您不希望使用这种自动化,显示和隐藏授权屏幕的手动步骤如下:
嵌入式 iOS:
let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
oauth2.authConfig.authorizeEmbeddedAutoDismiss = false
let web = try oauth2.authorizer.authorizeSafariEmbedded(from: <# view controller #>, at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
// inspect error or oauth2.accessToken / authParameters or do something else
web.dismissViewControllerAnimated(true, completion: nil)
}
macOS 上的模态表单:
let window = <# window to present from #>
let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
let sheet = try oauth2.authorizer.authorizeEmbedded(from: window, at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
// inspect error or oauth2.accessToken / authParameters or do something else
window.endSheet(sheet)
}
macOS 上的新窗口:
let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
let windowController = try oauth2.authorizer.authorizeInNewWindow(at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
// inspect error or oauth2.accessToken / authParameters or do something else
windowController.window?.close()
}
iOS/macOS 浏览器:
let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
try oauth2.authorizer.openAuthorizeURLInBrowser(url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
// inspect error or oauth2.accessToken / authParameters or do something else
}
macOS
有关如何在您的 Mac 应用中接收回调 URL 的示例,请参阅 OAuth2 示例应用 的 AppDelegate 类。如果授权将代码显示给用户,例如使用 Google 的 urn:ietf:wg:oauth:2.0:oob
回调 URL,您可以从用户的剪贴板中检索代码,并继续授权
let pboard = NSPasteboard.general()
if let pasted = pboard.string(forType: NSPasteboardTypeString) {
oauth2.exchangeCodeForToken(pasted)
}
流程
根据您需要的 OAuth2 流,您将想要使用正确的子类。有关 OAuth 基础的非常好解释:OAuth 圣经。
授权码授予
对于OAuth 2.0代码授权流程(response_type=code
),您应该使用OAuth2CodeGrant
类。这种流程通常用于可以保护其秘密的应用程序,例如服务器端应用程序,而不会在分布式二进制文件中使用。如果应用程序无法保护其秘密,例如分布式iOS应用程序,则可以使用隐式授权,或者在某些情况下,仍然使用代码授权,但省略客户端密钥。然而,从移动设备(包括客户端密钥)使用代码授权已经成为一种常见做法。
该类完全支持这些流程,如果客户端具有非空客户端密钥,它会自动创建一个“Basic”授权头。这意味着您很可能必须在其设置中指定client_secret
;如果没有,例如在Reddit的情况下,指定空字符串。如果网站要求在请求体中包含客户端凭据,请将clientConfig.secretInBody
设置为true,如下所述。
隐式授权
隐式授权(《response_type=token》)适用于无法保护其秘密的应用程序,例如分布式二进制文件或客户端Web应用程序。使用OAuth2ImplicitGrant
类接收令牌并执行请求。
在这里添加另一个代码示例会很不错,但它与代码授权几乎相同。
客户端凭证
双端流程允许应用程序通过其客户端ID和密钥进行授权。像往常一样实例化OAuth2ClientCredentials
,在设置字典中提供client_id
和客户端密钥(以及其他配置),然后就可以使用了。
用户名和密码
支持使用OAuth2PasswordGrant
子类进行资源拥有者密码凭证授权。创建一个实例,像上面那样设置其username
和password
属性,然后调用authorize()
。
特定网站的特性
一些网站可能不完全遵循OAuth2流程,例如Facebook返回数据方式不同,或Instagram及其它产品省略强制返回参数。框架通过创建特定网站的子类和/或配置细节来处理这些偏差。如果您需要传递额外的头信息或参数,可以根据以下方式在设置字典中提供这些信息。
let oauth2 = OAuth2CodeGrant(settings: [
"client_id": "...",
...
"headers": ["Accept": "application/vnd.github.v3+json"],
"parameters": ["duration": "permanent"],
] as OAuth2JSON)
高级设置
您将使用的与oauth2.authConfig
相关的最主要配置是是否使用嵌入式登录。
oauth2.authConfig.authorizeEmbedded = true
类似地,如果您想自己处理关闭登录界面(对于下文提到的较新的授权会话则不可行)。
oauth2.authConfig.authorizeEmbeddedAutoDismiss = false
一些网站还希望在请求数据中包含客户端ID/密钥组合,而不是在授权头中。
oauth2.clientConfig.secretInBody = true
// or in your settings:
"secret_in_body": true
有时您还需要提供额外的授权参数。这可以通过以下三种方式完成。
oauth2.authParameters = ["duration": "permanent"]
// or in your settings:
"parameters": ["duration": "permanent"]
// or when you authorize manually:
oauth2.authorize(params: ["duration": "permanent"]) { ... }
指定自定义HTTP头的方法与此类似。
oauth2.clientConfig.authHeaders = ["Accept": "application/json, text/plain"]
// or in your settings:
"headers": ["Accept": "application/json, text/plain"]
从iOS 9的2.0.1版本开始,将使用SFSafariViewController
进行嵌入式授权。从iOS 11的4.2版本开始,您可以选择使用这些较新的授权会话视图控制器。
oauth2.authConfig.ui.useAuthenticationSession = true
要恢复到旧的定制OAuth2WebViewController
,您不应该这样做,因为ASWebAuthenticationSession
的加密性更高。
oauth2.authConfig.ui.useSafariView = false
使用iOS 8及更老的OAuth2WebViewController时,如何定制返回按钮。
oauth2.authConfig.ui.backButton = <# UIBarButtonItem(...) #>
与Alamofire的使用
当使用Alamofire v4或更新版本以及OAuth2 v3或更新版本时,您将获得最佳体验。
动态客户端注册
支持动态客户端注册。如果在设置过程中设置了registration_url
但没有设置client_id
,则在继续到实际授权之前,自动尝试注册该客户端。注册返回的身份验证凭据将被存储到密钥链中。
OAuth2DynReg
类负责处理客户端注册。如果您需要手动注册,可以使用其register(client:callback:)
方法。注册参数来自客户端的配置。
let oauth2 = OAuth2...()
oauth2.registerClientIfNeeded() { error in
if let error = error {
// registration failed
}
else {
// client was registered
}
}
let oauth2 = OAuth2...()
let dynreg = OAuth2DynReg()
dynreg.register(client: oauth2) { params, error in
if let error = error {
// registration failed
}
else {
// client was registered with `params`
}
}
PKCE
PKCE支持由useProofKeyForCodeExchange
属性和设置字典中的use_pkce
键来控制。默认情况下禁用。启用时,每次授权请求都会为每个授权请求生成一个新的代码验证符字符串。
密钥链
该框架可以透明地使用iOS和macOS密钥链。它由useKeychain
属性控制,可以在初始化时使用keychain
设置字典键来禁用。默认情况下启用,如果您在初始化时没有将其关闭,密钥链将被查询以获取与授权URL相关的令牌和客户端凭据。如果您在初始化后关闭它,密钥链将被查询以查找现有的令牌,但新的令牌将不会写入密钥链。
如果您想要从密钥链中删除令牌,即完全注销用户,请调用forgetTokens()
。如果您已动态注册您的客户端并希望重新开始,您可以调用forgetClient()
。
理想情况下,访问令牌会包含一个“expires_in”参数,告诉您令牌的有效期有多长。如果缺少此参数,则在密钥链中找到令牌时,框架仍然会使用那些令牌,并不重新执行OAuth操作。如果您希望包装令牌假设未过期,您可以在设置中提供token_assume_unexpired: false
或将clientConfig.accessTokenAssumeUnexpired
设置为false。
以下是您可以用于更多控制的设置字典键
keychain
:是否使用密钥链的布尔值,默认为truekeychain_access_mode
:用于keychain kSecAttrAccessible属性的字符串值,默认为"憧AttrAccessibleWhenUnlocked",如果您需要当手机锁定时可以访问令牌,可以将其更改为例如"摊AttrAccessibleAfterFirstUnlock"。keychain_access_group
:用于keychain kSecAttrAccessGroup属性的字符串值,默认为nil。keychain_account_for_client_credentials
:在keychain中识别客户端凭据时要使用的名称,默认为"客户端凭据"。keychain_account_for_tokens
:在keychain中识别令牌时要使用的名称,默认为"当前令牌"。
安装
您可以使用Swift Package Manager、git或Carthage。首选方法是使用Swift Package Manager。
Swift Package Manager
在Xcode 11及其以上版本中,从Xcode菜单中选择"文件",然后选择"Swift Packages"」"添加包依赖...",粘贴此存储库的URL:https://github.com/p2/OAuth2.git
。选择一个版本,Xcode将完成其余步骤。
Carthage
通过Carthage进行安装很简单
github "p2/OAuth2" ~> 4.2
git
使用Terminal.app,克隆OAuth2存储库,最好将其放置到您的应用程序项目子目录中。
$ cd path/to/your/app
$ git clone --recursive https://github.com/p2/OAuth2.git
如果您正在使用git,则希望将其添加为子模块。一旦克隆完成,请在Xcode中打开您的应用程序项目,并将OAuth2.xcodeproj
添加到您的应用程序中。
现在将框架链接到您的应用程序
这三步执行时的需要
- 使您的应用程序也能够构建框架
- 将框架链接到您的应用程序中
- 在分发时将框架嵌入到您的应用程序中
许可证
这段代码在 Apache 2.0 许可协议 下发布,这意味着您可以在开源和闭源项目中使用它。由于没有 NOTICE
文件,所以您的产品中无需包含任何内容。