SafariViewController:如何从 URL 获取 OAuth 令牌?

SafariViewController: How to grab OAuth token from URL?

正在尝试将 Facebook OAuth 与 SafariViewController 结合使用。首先,我用 SafariViewController 打开 authURL,如果用户在 Safari 上登录到 Facebook,它将重定向他们和 return 一个带有特定服务令牌的 OAuth URL,例如Instagram

响应:https://www.facebook.com/connect/login_success.html#access_token=BLAHTOKENRESPONSE&expires_in=5114338

当 SafariViewController 重定向后,我想获取响应 URL 并存储它以便获取令牌。这是我的代码:

import SafariServices

let kSafariViewControllerCloseNotification = "kSafariViewControllerCloseNotification"

import UIKit

// facebook OAuth URL for service
let authURL = NSURL(string: "https://www.facebook.com/dialog/oauth?client_id=3627644767&redirect_uri=https://www.facebook.com/connect/login_success.html&scope=basic_info,email,public_profile,user_about_me,user_activities,user_birthday,user_education_history,user_friends,user_interests,user_likes,user_location,user_photos,user_relationship_details&response_type=token")

class ViewController: UIViewController, SFSafariViewControllerDelegate {

    var safariVC: SFSafariViewController?
    @IBOutlet weak var loginButton: UIButton!

    @IBAction func loginButtonTapped(sender: UIButton) {
        safariVC = SFSafariViewController(URL: authURL!)
        safariVC!.delegate = self
        self.presentViewController(safariVC!, animated: true, completion: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // not firing the safariLogin function below
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.safariLogin(_:)), name: kSafariViewControllerCloseNotification, object: nil)
    }

    func safariLogin(notification: NSNotification) {
        print("Safari Login call")
        // get the url form the auth callback
        let url = notification.object as! NSURL
        print(url)
        self.safariVC!.dismissViewControllerAnimated(true, completion: nil)
    }

    func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
        print("application call")
        // just making sure we send the notification when the URL is opened in SFSafariViewController
        if (sourceApplication == "com.application.SafariViewTest") {
            NSNotificationCenter.defaultCenter().postNotificationName(kSafariViewControllerCloseNotification, object: url)
            return true
        }
        return true
    }  

}

它打开 authURL 并重定向到正确的响应 URL,但观察者不会触发 safariLogin 函数来获取 URL。任何帮助将不胜感激!

非常感谢!

想通了。一些方法是 iOS 9 之前的,现在已弃用。我还在 ViewController 中创建了 application 函数,而它本应在 AppDelagate.swift 中定义。例如

添加在 AppDelegate.swift

末尾
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {

        print("app: \(app)")
        // print OAuth response URL
        print("url: \(url)")
        print("options: \(options)")

        if let sourceApplication = options["UIApplicationOpenURLOptionsSourceApplicationKey"] {
            if (String(sourceApplication) == "com.testApp.Incognito") {
                NSNotificationCenter.defaultCenter().postNotificationName(kSafariViewControllerCloseNotification, object: url)
                return true
            }
        }
        return true
    }

ViewController.swift

import SafariServices

let kSafariViewControllerCloseNotification = "kSafariViewControllerCloseNotification"

// facebook OAuth URL
let authURL = NSURL(string: "https://www.facebook.com/dialog/oauth?client_id=3627644767&redirect_uri=https://www.facebook.com/connect/login_success.html&scope=basic_info,email,public_profile,user_about_me,user_activities,user_birthday,user_education_history,user_friends,user_interests,user_likes,user_location,user_photos,user_relationship_details&response_type=token")

class ViewController: UIViewController, SFSafariViewControllerDelegate {

    var safariVC: SFSafariViewController?
    @IBOutlet weak var loginButton: UIButton!

    @IBAction func loginButtonTapped(sender: AnyObject) {
        safariVC = SFSafariViewController(URL: authURL!)
        safariVC!.delegate = self
        self.presentViewController(safariVC!, animated: true, completion: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.safariLogin(_:)), name: kSafariViewControllerCloseNotification, object: nil)
    }

    func safariLogin(notification: NSNotification) {
        // get the url from the auth callback
        let url = notification.object as! NSURL
        // Finally dismiss the Safari View Controller with:
        self.safariVC!.dismissViewControllerAnimated(true, completion: nil)
    } 

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func safariViewControllerDidFinish(controller: SFSafariViewController) {
        controller.dismissViewControllerAnimated(true) { () -> Void in
            print("You just dismissed the login view.")
        }
    }

    func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
        print("didLoadSuccessfully: \(didLoadSuccessfully)")

    }
}

iOS12+

iOS 12 Beta 已经弃用 SFAuthenticationSession(见下文)以支持 ASWebAuthenticationSession. It looks like it is used exactly the same way but requires the new AuthenticationServices 框架。

iOS 11

iOS11 在网上介绍SFAuthenticationSession which is so much easier to handle. Given its nature this beta API may still change but there already are a couple of examples (1, 2)。首先,您需要一个用身份验证请求的结果调用的完成处理程序:

let completion : SFAuthenticationSession.CompletionHandler = { (callBack:URL?, error:Error?) in
    guard error == nil, let successURL = callBack else {
        return
    }

    let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({[=10=].name == "oauth_token"}).first

    // Do what you have to do...
}

然后您只需创建一个 SFAuthenticationSession 并启动它。

let authURL = "https://the.service.you/want/toAuthorizeWith?..."
let scheme = "YOURSCHEME://"
let authSession = SFAuthenticationSession(url: authURL, callbackURLScheme: scheme, completionHandler: completion)
authSession.start()

iOS 10 岁及之前

正如一些人在评论中指出的那样,已接受的答案不完整,无法单独使用。逛 Strawberry Code 的博客 post 可以找到一个 link to the related GitHub project。该项目的 README.MD 解释了设置的关键部分,即将重定向 URI 添加到 Info.plist。所以整个事情是这样的:

AppDelegate.swift

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {

   if let sourceApplication = options[.sourceApplication] {
       if (String(describing: sourceApplication) == "com.apple.SafariViewService") {
            NotificationCenter.default.post(name: Notification.Name("CallbackNotification"), object: url)
            return true
        }
    }

    return false
}

ViewController.swift

注册通知以在某个合理的地方呼叫您的处理程序。我建议不要在 viewDidLoad() 中这样做,而是在您实际展示 SFSafariViewController.

之前这样做
NotificationCenter.default.addObserver(self, selector: #selector(safariLogin(_:)), name: Notification.Name("CallbackNotification"), object: nil)
let safariVC = SFSafariViewController(URL: authURL)
safariVC.delegate = self
self.present(safariVC, animated: true, completion: nil)

然后删除处理程序中的遵守:

@objc func safariLogin(_ notification : Notification) {

    NotificationCenter.default.removeObserver(self, name: Notification.Name("CallbackNotification"), object: nil)

    guard let url = notification.object as? URL else {
        return
    }

    // Parse url ...

}

请记住,用户可以通过点击 Done 按钮来关闭 SFSafariViewController,因此请务必采用 SFSafariViewControllerDelegate 协议并像这样删除遵守规则:

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
    NotificationCenter.default.removeObserver(self, name: Notification.Name("CallbackNotification"), object: nil)
}

Info.plist

要使其全部正常工作,您需要将重定向 URI 方案添加到 Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.YOUR.BUNDLE.IDENTIFIER</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>YOURSCHEME</string>
        </array>
    </dict>
</array>

当然,YOURSCHEME 必须与您注册的重定向 URI 的方案相匹配,您正在尝试对其进行授权。