如何通過 OAuth 2.0 使 iOS Apps 集成 LinkedIn 登錄功能?

社交網絡早已成為人們日常生活的一部分。其實,社交網絡也是編程生活的一部分,大多數 App 必須通過某種方式與社交網絡交互,傳送或接收與用戶相關的數據。大多數情況下,用戶需要登錄某種社交網絡,授權 App 代表自己進行請求。

目前,此類社交網絡的種類非常豐富,以 Facebook 與 Twitter 最為常用。而且,iOS 系統內置了對這兩款社交網絡的支持。然而,對于其他類型的社交網絡,開發者必須投入更多的努力,以使 App 獲得授權訪問這些社交網絡,繼而運行經過授權的合法請求。LinkedIn 就是這樣一種社交網絡,在本教程中,我們會了解如何為 App 授權,使之與 LinkedIn 的服務器交換受保護的數據。

為 iOS App 授權以訪問 LinkedIn,并根據后者提供的 API 運行特定的操作,可以通過兩種方法實現。方法一:使用 LinkedIn 支持的 OAuth 2.0 協議。方法二:使用 LinkedIn 提供的 iOS SDK。與所有第三方 SDK 一樣,LinkedIn 提供的 SDK 必須集成到項目中,經過合理的配置才能使用。

linked-in-sign-in

在本文中,我們將僅專注于第一種方法。因此,我們將學習 LinkedIn 與 OAuth 2.0 指南中與此相關的指定內容,包括讓用戶通過 App(是任何 App,而不僅僅是 iOS 系統)登錄 LinkedIn 并為 App 授權執行進一步請求的必要流程。盡管 LinkedIn iOS SDK 也是很好的選擇,但筆者更喜歡 OAuth 方法,原因有三:

  1. 筆者個人更偏愛此類任務:使用 REST API 調用與服務器進行直接的交流,以順利完成授權過程。
  2. LinkedIn 網站中有關 LinkedIn iOS SDK 的介紹相當明確詳盡,因此筆者認為基于相同主題寫的教程恐怕益處不大。
  3. 筆者認為,使用 LinkedIn iOS SDK 時存在一個缺陷:官方的 LinkedIn App 必須已經安裝在設備中,否則登錄與授權過程就無法完成。如果某個 App 需要獲得用戶的 LinkedIn 主頁信息,但用戶并不想安裝 LinkedIn 官方應用,就會造成不便。

關于 OAuth 2.0 協議,能說的實在太多了。讀者最好還是登錄官網仔細研讀一下。簡而言之,為了成功完成登錄與授權過程,本教程將會遵循以下步驟:

  • 必要地,我們將在 LinkedIn 開發者網站創建一個新的 App。從而得到完成后續過程必備的兩個重要密匙(Client ID 與 Client Secret)。
  • 通過一個 Web 視圖,讓用戶登錄其 LinkedIn 賬戶。
  • 根據以上所得,再加上一些必要數據,向 LinkedIn 服務器索取授權碼。
  • 與一個訪問令牌交換授權碼。

訪問令牌是與 OAuth 交互的必要條件。通過一個有效的令牌,我們便能向 LinkedIn 服務器發送經過授權的請求。并根據 App 的性質,“get”或“post” 數據到用戶的主頁。

在繼續閱讀之前,請確保你理解 OAuth 2.0 的工作原理,以及它的流(flow)。如果必要,閱讀其他資源以獲取更多信息(比如這兒這兒這兒)。

說了這么多,讓我們進入正題,介紹本教程的演示應用,然后進入具體實現。筆者相信,我們將要學習的內容是趣味無窮的。

作為參考,以下是 LinkedIn 官方文檔的鏈接:

演示 App 概覽

我們在本教程中將要實現的演示 App 由兩部分視圖控制器組成:第一個(默認的 ViewController 類)只包含三個按鈕:

  1. 一個名為 LinkedIn Sign In(LinkedIn 登錄)的按鈕,用于啟動登錄與授權流程。
  2. 一個名為 Get my profile URL(獲得我的主頁 URL)的按鈕,用于執行一個經過授權的請求,使用訪問令牌獲得用戶主頁的 URL。
  3. 一個展示主頁 URL 的按鈕,點擊之后會在 Safari 中打開用戶的 LinkedIn 主頁。

默認情況下,只會啟用第一個按鈕。實際上,只要還未獲得訪問令牌,該按鈕就會一直可用。在其他情況下,第一個按鈕會被禁用,同時啟用第二個按鈕。第三個按鈕是隱藏的,只有當得到(通過第二個按鈕)用戶主頁的 URL 時,才會可見。

view-controller-signin

第二個視圖控制器會包含一個 Web 視圖。通過該試圖,你可以登錄 LinkedIn 賬戶,這樣認證與授權過程才能成功進行。當獲得用于向 LinkedIn 發送合法請求的訪問令牌后,該視圖控制器就會被移除。

t47_2_user_sign_in

與往常一樣,我們不需要從頭開始創建項目。你可以下載一個啟動項目,在此基礎上繼續搭建。

基本上,我們的主要努力將專注于獲取訪問令牌。我們會遵循 OAuth 2.0 協議以及 LinkedIn 指南的指定,一步一步地完成所有必備流程。獲得訪問令牌之后,我們會繼續解釋如何向 LinkedIn 發送合法請求,以獲得授權用戶公共主頁的 URL。成功得到 URL 之后,我們會請用前面提到的第三個按鈕,將主頁內容顯示在 Safari 瀏覽器中。

在你繼續閱讀之前,請確保已經下載啟動項目,打開它并熟悉它的。準備就緒之后,請繼續往下看。

LinkedIn 開發者網站—— 創建新的 App

實現 OAuth 2.0 登錄流程的第一步,是在 LinkedIn 開發者網站創建一個新的 App 記錄。為此,你僅需訪問此鏈接。如果你還沒有登錄 LinkedIn 主頁,你將收到提示以完成登錄操作。

注意:如果你在下面的步驟中使用 Safari 出現問題,請選擇其他瀏覽器。我使用的是 Chrome 瀏覽器。

登錄之后,找到網站“我的應用(My Applications)”部分,你會發現一個名為“創建應用(Create Application)”的黃色按鈕。點擊它開始創建新的應用,之后我們會將它與 iOS App 進行聯結。

t47_3_create_app_button

在接下來出現的表格中,填寫所有欄目。如果需要填寫公司名稱或上傳應用 logo,不用擔心,輸入一些虛假信息也可。之后,接受使用條款,點擊提交按鈕。請一定要在帶紅色星號的欄目中輸入內容,否則你將無法繼續。以下為示例:

t47_4_create_new_app

我們的目標是抵達下一個頁面:

t47_5_app_settings

如你在上面的截圖中所見,在此頁面可以看到 Client ID 與 Client Secret 的值。請不要關閉該窗口,因為接下來的步驟中會用到它們。你可以使用窗口左側的菜單選項,隨意探索應用的設置。

此處,除了得到 Client 密匙(Client ID 與 Client Secret 的值),我們還要完成的另一項重要任務,是在“合法重定向 URLs(Authorized Redirect URLs)”一欄填入合適的值。當客戶端 App 試圖刷新現有的訪問令牌,用戶無需通過 Web 瀏覽器重新登錄,使用合法重定向 URL 即可。OAuth 流會自動使用該 URL 將 App 重定向。在正常的登錄過程中,客戶端 App 與 LinkedIn 服務器會交換該 URL,同時取得授權碼與訪問令牌。總之,該值不能為空,稍后會用來與服務器進行交換,因此必須定義它。

重定向 URL 不需要是真實存在的 URL,可以是任何以 “https://” 開頭的值。在此,筆者將其賦值如下。你可以將其改為任何你希望的值。

https://com.appcoda.linkedin.oauth/oauth

如果你使用了一個不同的 URL, 千萬記得對后面出現的代碼進行相應的修改。

在“OAuth 2.0”一節寫入合法重定向 URL 后,必須點擊添加按鈕,保證將其加入 App 中。

t47_6_authorized_redirect_url

此外,記得點擊屏幕底部的更新按鈕。

至于有關訪問權限的選項,保留基本選項即可,因為其完全滿足本教程的需求。當然,你也可以選擇更多權限,或在演示 App 準備就緒之后再做修改。請注意,如果 App 請求的最初權限遭到改動,用戶必須重新登錄以認可這些改動。

開始授權過程

現在,打開 Xcode 中的啟動項目,我們即將開始實現,并最終完成 OAuth 2.0 流。不過,在開始之前,請選擇項目導航欄(Project Navigator)中的 WebViewController.swift 文件,打開它。在該類的頭部,你會看到兩個名為 linkedInKey 與 linkedInSecret 的變量。你需要將之前從 LinkedIn 開發者網站得到的 Client ID 與 Client Secret 值分別賦值給這兩個變量(簡單的復制、黏貼即可)。

t47_7_assigned_keys

本步的主要目的,是準備好用來獲取授權碼的請求,并通過一個 Web 視圖加載它。界面生成器(Interface Builder)中的 WebViewController 已經包含了一個 Web 視圖,因此我們將以 WebViewController 類為基礎構建視圖。用于獲取授權碼的請求必須包含以下參數:

  • response_type:取值為恒定的標準值:code。
  • client_id:取值為來自 LinkedIn 開發者網站的 Client ID,之后會賦值給項目中的 linkedInKey 屬性。
  • redirect_uri:取值為在前一節指定的合法重定向 URL。請確保在后面的代碼段中填入相應的值。
  • state:取值為唯一的字符串,用于預防跨站請求偽造(CSRF)。
  • scope:取值為 App 請求的訪問權限列表,以 URL 形式編碼。

下面,介紹代碼的具體實現。首先,在 WebViewController 類下創建一個名為 startAuthorization() 的新函數。該函數的第一個任務是根據上文的描述為請求參數賦值。

func startAuthorization() {
    // Specify the response type which should always be "code".
    let responseType = "code"
 
    // Set the redirect URL. Adding the percent escape characthers is necessary.
    let redirectURL = "https://com.appcoda.linkedin.oauth/oauth".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.alphanumericCharacterSet())!
 
    // Create a random string based on the time interval (it will be in the form linkedin12345679).
    let state = "linkedin\(Int(NSDate().timeIntervalSince1970))"
 
    // Set preferred scope.
    let scope = "r_basicprofile"
 
 
}

請注意:不是簡單地將合法重定向 URL 賦值給 redirectURL 變量。我們需要將 URL 中的特殊符號通過 URL 編碼替換為百分比編碼字符。因此,下面的鏈接:

https://com.appcoda.linkedin.oauth/oauth

將會轉換為:

https%3A%2F%2Fcom.appcoda.linkedin.oauth%2oauth

(See more about URL-encoding here).

點此了解 URL 編碼的更多信息。)

其次,state 變量必須包含一個唯一且莫測的字符串。在上面的代碼中,我們將“linkedin”字符串與當前時間戳(自1970年以來的時間間隔)的整數部分進行聯結,以此確保字符串的唯一性。你也可以生成隨機字符,再將其附加到 state 字符串上。

最后,將 scope 賦值為“r_basicprofile”,后者與之前在 LinkedIn 開發者網站設定的 App 訪問權限相匹配。當你設置訪問權限時,請確保與官方文檔中的規定一致。

Our next step is to compose the authorization URL. Note that the https://www.linkedin.com/uas/oauth2/authorization URL must be used for the request, which is already assigned to the authorizationEndPoint property.

下一步,創建授權 URL。請注意,URL https://www.linkedin.com/uas/oauth2/authorization 必須用于該請求,而該 URL 已經賦做 authorizationEndPoint 屬性的值。

回到代碼:

func startAuthorization() {
    ...

    // Create the authorization URL string.
    var authorizationURL = "\(authorizationEndPoint)?"
    authorizationURL += "response_type=\(responseType)&"
    authorizationURL += "client_id=\(linkedInKey)&"
    authorizationURL += "redirect_uri=\(redirectURL)&"
    authorizationURL += "state=\(state)&"
    authorizationURL += "scope=\(scope)"

    print(authorizationURL)
}

此處,筆者添加了打印命令,是為了讓讀者親眼看到該請求最終是如何形成的。

最終,我們需要在 Web 視圖中加載該請求。請記住,只有前文所述的請求配置得當,用戶才能通過 Web 視圖成功登錄。否則,LinkedIn 將返回錯誤消息,導致無法進行下一步操作。

因此,請確保正確拷貝了 Client Key、Client Secret,以及統一的合法重定向 URL。

在 Web 視圖中加載該請求只需短短幾行代碼:

func startAuthorization() {
    ...

    // Create a URL request and load it in the web view.
    let request = NSURLRequest(URL: NSURL(string: authorizationURL)!)
    webView.loadRequest(request)
}

在結束本節之前,我們必須調用上面的函數。可以通過 viewDidLoad(_: ) 函數進行調用:

override func viewDidLoad() {
    ...

    startAuthorization()
}

此時,你終于可以運行 App,測試其是否成功了。如果你根據筆者的指導配置正確,應該可以看到以下頁面:

t47_2_user_sign_in

不過,先別急著登錄 LinkedIn 賬號,本節還有一部分工作未完成。然而,如果你看到了登錄表格,說明你已經成功發送了獲取授權碼的請求。登錄之后,LinkedIn 會向瀏覽器(在本例中,也即我們的 Web 視圖)返回一個授權碼。

除此之外,還會在控制臺打印出 authorizationURL(授權 URL)字符串:

t47_8_authorization_request

Getting an Authorization Code

獲取授權碼

授權碼請求函數準備就緒,且在 Web 視圖中成功加載之后,我們可以繼續執行 webView(:shouldStartLoadWithRequest:navigationType) 委托函數。在此函數中,我們會捕獲來自 LinkedIn 的響應,并從中抽取出渴望已久的授權碼。

包含授權碼的響應如下所示:

http://com.appcoda.linkedin.oauth/oauth?<strong>code=AQSetQ252oOM237XeXvUreC1tgnjR-VC1djehRxEUbyZ-sS11vYe0r0JyRbe9PGois7Xf42g91cnUOE5mAEKU1jpjogEUNynRswyjg2I3JG_pffOClk</strong>&state=linkedin1450703646

因此,我們需要將該字符串分為多個部分,隔離出“code”的值。不過,有兩點注意:其一,我們必須確保委托函數中的 URL 是我們感興趣的。其二,必須確保授權碼的確存在于該 LinkedIn 響應中。代碼的實現如下:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    let url = request.URL!
    print(url)

    if url.host == "com.appcoda.linkedin.oauth" {
        if url.absoluteString.rangeOfString("code") != nil {

        }
    }

    return true
}

首先,通過請求參數獲得該 URL。接著,檢查 URL 的主機屬性值以確保這是我們需要的 URL(也即在 LinkedIn 開發者網站設定的重定向 URL)。如果是,請求字符串中 “code” 所在的范圍,以驗證該 URL 是否真的包含授權碼。如果返回不為空,則證明授權碼的確存在。

將 URL 字符串分為多個部分并不難。為了簡化步驟,筆者將該任務分為兩步:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    let url = request.URL!
    print(url)

    if url.host == "com.appcoda.linkedin.oauth" {
        if url.absoluteString.rangeOfString("code") != nil {
            // Extract the authorization code.
            let urlParts = url.absoluteString.componentsSeparatedByString("?")
            let code = urlParts[1].componentsSeparatedByString("=")[1]

            requestForAccessToken(code)
        }
    }

    return true
}

除了上面新出現的兩行代碼,你也肯定注意到了對 requestForAccessToken(_: ) 函數的調用。這是我們將在下一部分實現的自定義函數。在此函數中,我們會用此處獲得的授權碼讀取訪問令牌。

如你所見,只差一步,我們就能使用 OAuth 2.0 流獲取訪問令牌了。此處扼要重述一下之前的步驟:首先,我們成功創建了獲取授權碼的請求。接著,作為授權過程的一部分,用戶通過該請求連接他們的 LinkedIn 賬號。最后,得到并抽取出授權碼。

如果你想對目前的 App 進行測試,只需注釋掉 requestForAccessToken(_: ) 函數的調用部分即可。你大可以在任意位置添加打印命令,從而深刻理解每個步驟的作用。

Requesting for the Access Token

信息結構,創作我們的信息就是如此,我已經很是在此基礎上我們創作就是token整個結構就是做這些事情的

請求訪問令牌

此前,我們與 LinkedIn 服務器的所有交流都是通過 Web 視圖進行的。從現在起,我們將僅通過簡便的 RESTful 請求(也即 POST 與 GET 請求)與服務器交流。更具體地說,我們會發起一個 POST 請求來獲取訪問令牌,之后再用 GET 請求獲得用戶主頁的 URL。

話雖如此,現在要先創建在上一部分末尾提過的新的自定義函數:requestForAccessToken()。在此函數內部,我們將執行三個任務:

  1. 準備好 POST 請求的參數。
  2. 初始化并配置一個可變的 URL 請求對象(NSMutableURLRequest)。
  3. 實例化一個 NSURLSession 對象,繼而執行一個數據任務請求。在得到恰當的響應之后,我們將訪問令牌存儲在用戶默認的字典中。

準備 POST 請求參數

與獲取授權碼的請求準備相似,為了獲得訪問令牌,我們需要在請求中 POST 特定的參數與其對應的值。這些參數包括:

  • grant_type: It’s a standard value that should always be: authorization_code.
  • code: The authorization code acquired in the previous part.
  • redirect_uri: It’s the authorized redirection URL we’ve talked about many times earlier.
  • client_id: The Client Key value.
  • client_secret: The Client Secret Value.
  • grant_type:取值為恒定的標準值:authorization_code。
  • code:取值為在上一部分獲得的授權碼。
  • redirect_uri:取值為前面多次提到的合法重定向 URL。
  • client_id:取值為 Client Key 的值。
  • client_secret:取值為 Client Secret 的值。

在上一部分得到的授權碼將在新函數中用作參數。首先,讓我們為參數“grant_type”與“redirect_uri”賦值:

   func requestForAccessToken(authorizationCode:     String) {
    let grantType = "authorization_code"

    let redirectURL = "https://com.appcoda.linkedin.oauth/oauth".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.alphanumericCharacterSet())!
}

其他所有參數的值 App 都已經知道了,因此,我們可以將其整合為一個字符串:

func requestForAccessToken(authorizationCode: String) {
    ...

    // Set the POST parameters.
    var postParams = "grant_type=\(grantType)&"
    postParams += "code=\(authorizationCode)&"
    postParams += "redirect_uri=\(redirectURL)&"
    postParams += "client_id=\(linkedInKey)&"
    postParams += "client_secret=\(linkedInSecret)"
}

如果你曾用 NSMutableURLRequest 類創建過 POST 請求,那你一定知道 POST 參數無法以字符串的形式傳送。它們必須轉化為 NSData 對象,再賦值給請求的 HTTPBody 部分(后文會有解釋)。因此,讓我們按照要求轉化 postParams:

func requestForAccessToken(authorizationCode: String) {
    ...

    // Convert the POST parameters into a NSData object.
    let postData = postParams.dataUsingEncoding(NSUTF8StringEncoding)
}

準備請求對象

準備好 POST 參數之后,我們可以繼續初始化并配置 NSMutableURLRequest 對象。初始化時會用到獲取訪問令牌所需的 URL(https://www.linkedin.com/uas/oauth2/accessToken) ,而后者已經賦值給 accessTokenEndPoint 屬性。

func requestForAccessToken(authorizationCode: String) {
    ...    

    // Initialize a mutable URL request object using the access token endpoint URL string.
    let request = NSMutableURLRequest(URL: NSURL(string: accessTokenEndPoint)!)
}

Next, it’s time to “say” to the request object what kind of request we want to make, as well as to pass it the POST parameters:

接下來,告訴請求對象我們想要創建的請求類型,并傳入 POST 參數:

func requestForAccessToken(authorizationCode: String) {
    ...

    // Indicate that we're about to make a POST request.
    request.HTTPMethod = "POST"

    // Set the HTTP body using the postData object created above.
    request.HTTPBody = postData
}

根據 LinkedIn 文檔,請求的 Content-Type 部分需要設置為 application/x-www-form-urlencoded:

func requestForAccessToken(authorizationCode: String) {
    ...

    // Add the required HTTP header field.
    request.addValue("application/x-www-form-urlencoded;", forHTTPHeaderField: "Content-Type")
}

終于,請求對象的必要配置完成了。現在可以使用它了。

Performing the request

執行請求

我們將把用于獲取訪問令牌的請求實現為 NSURLSession 類的對象。通過該對象,創建一個數據任務請求,并在完成處理程序(completion handler)內部處理 LinkedIn 服務器的響應:

func requestForAccessToken(authorizationCode: String) {
    ...

    // Initialize a NSURLSession object.
    let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())

    // Make the request.
    let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in

    }

    task.resume()
}

如果請求成功,LinkedIn 服務器將會返回包含訪問令牌的 JSON 數據。因此,我們的任務是得到該 JSON 數據,將之轉化為字典對象,然后抽取出訪問令牌。當然,這一切只有在返回的 HTTP 狀態碼是 200,也即請求成功時,才能進行。

func requestForAccessToken(authorizationCode: String) {    
    ...

    // Make the request.
    let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
        // Get the HTTP status code of the request.
        let statusCode = (response as! NSHTTPURLResponse).statusCode

        if statusCode == 200 {
            // Convert the received JSON data into a dictionary.
            do {
                let dataDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)

                let accessToken = dataDictionary["access_token"] as! String
            }
            catch {
                print("Could not convert JSON data into a dictionary.")
            }
        }
    }

    task.resume()
}
我很好奇

此類操作可能拋出異常,將json 數據的倒換就是加載我們呢自身的SDK,接入信息就是從來不會有很多的考很多事情就是自己做起來用戶就是會考
請注意,轉化發生在一個 do-catch 語句內部,因為從 Swift 2.0 開始,此類操作可能拋出異常(并不存在錯誤參數)。在我們的演示 App 中,無需特別考慮出現異常的情況,因此可以向控制器發送一條信息,表示轉化失敗。如果一切運行順利,我們就將 JSON 數據(閉包中的數據參數)轉化為字典(dataDictionary 對象),之后就可以直接讀取訪問令牌。

接下來做什么呢?將字典保存在用戶默認的字典中,然后移除視圖控制器:

func requestForAccessToken(authorizationCode: String) {    
    ...

    // Make the request.
    let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
        // Get the HTTP status code of the request.
        let statusCode = (response as! NSHTTPURLResponse).statusCode

        if statusCode == 200 {
            // Convert the received JSON data into a dictionary.
            do {
                ...

                NSUserDefaults.standardUserDefaults().setObject(accessToken, forKey: "LIAccessToken")
                NSUserDefaults.standardUserDefaults().synchronize()

                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    self.dismissViewControllerAnimated(true, completion: nil)
                })                
            }
            catch {
                print("Could not convert JSON data into a dictionary.")
            }
        }
    }

    task.resume()
}

注意,視圖控制器會在主線程中移除。請永遠牢記,與 UI 相關的改動必須發生在 App 的主線程中,而不是背景線程中。而上面顯示的完成處理程序(閉包)則永遠在背景線程中執行。

Our ultimate goal has been finally achieved! We managed to acquire the access token that will “unlock” several API features.

我們的終極目標終于完成啦!得到訪問令牌之后,可以“解鎖”許多 API 功能。

獲得用戶主頁的 URL

接下來,我們將演示如何用訪問令牌獲得用戶主頁的 URL,并在 Safari 瀏覽器中打開它。然而,在此之前,讓我們先討論一點別的問題。當你啟動 App 時,你有兩個選擇,如下圖所示:

view-controller-signin

默認情況下,LinkedIn Sign In (LinkedIn 登錄)按鈕是啟用的,而 Get my profile URL(獲得我的主頁 URL)按鈕是禁用的。既然現在已經得到了訪問令牌,我們需要啟用第二個按鈕,同時禁用第一個按鈕。這要如何完成呢?

一種實現方式是使用委托模式,通過一個委托函數通知 ViewController 類:訪問令牌已經得到,請啟用第二個按鈕。另一種方式是從 WebViewController 類中 Post 一個自定義通知(NSNotification 對象),在 ViewController 類中監聽該通知。其實,兩種方法都可以實現。但是,還有一種更為簡單的方法三:在 ViewController 出現時,檢查訪問令牌是否存在于用戶默認的字典中。如果存在,就禁用登錄按鈕,啟用第二個按鈕。否則,就保持不變。

此處,我們會在 ViewController 類中實現一個新的小函數來進行檢查。請注意,我們還設置了第三個默認隱藏的按鈕(也即 btnOpenProfile IBOutlet 屬性)。當得到用戶主頁的 URL 時,該按鈕就會變為可見,并以此 URL 字符串作為其標題(后文會有示例)。

Now, let’s define this new function:

現在,先來定義這個新函數:

func checkForExistingAccessToken() {
    if NSUserDefaults.standardUserDefaults().objectForKey("LIAccessToken") != nil {
        btnSignIn.enabled = false
        btnGetProfileInfo.enabled = true
    }
}

我們會在 viewWillAppear(_: ) 方法中調用該函數:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    checkForExistingAccessToken()
}

之后,App 就能合理地啟用或禁用 ViewController 中的兩個按鈕了。

接下來,讓我們聚焦于 getProfileInfo(_: ) IBAction 方法。此方法會在 Get my profile URL(獲得我的主頁 URL)按鈕被點擊時執行。屆時,我們可以向 LinkedIn 服務器發送 GET 請求,使用訪問令牌獲得用戶主頁的 URL。此處采用的方法與在上一部分創建獲取訪問令牌的請求時所用的方法非常相似。

現在,讓我們從指定請求的 URL 字符串開始吧。請注意,當你不是很確定自己需要什么 URL,或者指定哪些參數時,大可以尋求官方文檔的幫助。

@IBAction func getProfileInfo(sender: AnyObject) {
    if let accessToken = NSUserDefaults.standardUserDefaults().objectForKey("LIAccessToken") {
        // Specify the URL string that we'll get the profile info from.
        let targetURLString = "https://api.linkedin.com/v1/people/~:(public-profile-url)?format=json"
    }
}

此處,作為額外措施,我們再一次檢查了訪問令牌是否存在。通過 if-let 語句,如果訪問令牌存在,我們便將其賦值給 accessToken 常量。而且,上面的 URL 會返給我們用戶主頁的 URL。不要忘記,在執行這類請求之前,必須獲得適當的權限。在本演示案例中,我們已經獲得了訪問用戶基本介紹信息的權限。

接下來,創建一個新的 NSMutableURLRequest 對象,并以“GET”方法作為理想的 HTTP 方法。此外,還需指定一個 HTTP 頭字段,此處將用訪問令牌為其賦值。

@IBAction func getProfileInfo(sender: AnyObject) {
    if let accessToken = NSUserDefaults.standardUserDefaults().objectForKey("LIAccessToken") {
        ...

        // Initialize a mutable URL request object.
        let request = NSMutableURLRequest(URL: NSURL(string: targetURLString)!)

        // Indicate that this is a GET request.
        request.HTTPMethod = "GET"

        // Add the access token as an HTTP header field.
        request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")        
    }
}

最后,再一次地,使用 NSURLSession 與 NSURLSessionDataTask 類創建該請求:

@IBAction func getProfileInfo(sender: AnyObject) {
    if let accessToken = NSUserDefaults.standardUserDefaults().objectForKey("LIAccessToken") {
        ...

        // Initialize a NSURLSession object.
        let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())

        // Make the request.
        let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in

        }

        task.resume()
    }
}

如果請求成功(也即 HTTP 狀態碼為 200),閉包中的數據參數將會包含服務器返回的 JSON 數據。與之前一樣,我們必須將此 JSON 數據轉化為字典,才能最終抽取出用戶主頁的 URL 字符串。

@IBAction func getProfileInfo(sender: AnyObject) {
    if let accessToken = NSUserDefaults.standardUserDefaults().objectForKey("LIAccessToken") {
        ...

        // Make the request.
        let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
            // Get the HTTP status code of the request.
            let statusCode = (response as! NSHTTPURLResponse).statusCode

            if statusCode == 200 {
                // Convert the received JSON data into a dictionary.
                do {
                    let dataDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)

                    let profileURLString = dataDictionary["publicProfileUrl"] as! String
                }
                catch {
                    print("Could not convert JSON data into a dictionary.")
                }
            }
        }

        task.resume()
    }
}

現在,回到之前提到過的一點內容:profileURLString 的值將會賦給 btnOpenProfile 按鈕的標題,該按鈕也會變成可見。還記得不?我們現在的工作都是在背景線程中進行的,因此,我們還需將其加入主線程中:

@IBAction func getProfileInfo(sender: AnyObject) {
    if let accessToken = NSUserDefaults.standardUserDefaults().objectForKey("LIAccessToken") {
        ...

        // Make the request.
        let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
            // Get the HTTP status code of the request.
            let statusCode = (response as! NSHTTPURLResponse).statusCode

            if statusCode == 200 {
                // Convert the received JSON data into a dictionary.
                do {
                    ...

                    dispatch_async(dispatch_get_main_queue(), { () -> Void in
                        self.btnOpenProfile.setTitle(profileURLString, forState: UIControlState.Normal)
                        self.btnOpenProfile.hidden = false

                    })
                }
                catch {
                    print("Could not convert JSON data into a dictionary.")
                }
            }
        }

        task.resume()
    }
}

現在,運行 App,如果你成功得到了訪問令牌,在點擊 Get my profile URL(獲得我的主頁 URL)按鈕后不久,你就能看到自己主頁的 URL 顯示在第三個按鈕的位置。

t47_9_get_profile_url

在 Safari 瀏覽器查看主頁

現在,通過使用訪問令牌與 LinkedIn API,我們得到了用戶主頁的 URL。接下來就該驗證其是否正確了。既然已將此 URL 設置為一個按鈕的標題,最快的驗證方法莫過于打開它了。具體的實現方法箱單簡單,因此筆者也不必要多言:

@IBAction func openProfileInSafari(sender: AnyObject) {
    let profileURL = NSURL(string: btnOpenProfile.titleForState(UIControlState.Normal)!)
    UIApplication.sharedApplication().openURL(profileURL!)
}

The last line above will trigger the appearance of Safari, which will load and display the profile webpage.

上面最后一行代碼會觸發 Safari 瀏覽器,后者會加載并展示用戶的主頁。

t47_10_open_profile

你可能已經發現,教程已經步入尾聲,卻仍然沒有提及廢除或刷新訪問令牌的內容。其實原因如下:關于廢除訪問令牌,LinkedIn 并未提供任何相關的 API。因此,如果你需要停止 App 發送合法請求,最好的做法應該是從存儲機制(數據庫,用戶默認設置等)中刪除之。除此之外,一個訪問令牌的有效期大約為60天(在筆者撰寫本文之時,官網文檔是如此規定的)。LinkedIn 建議,在此時間范圍到期之前,刷新訪問令牌。刷新的操作非常簡單,你只需要從頭進行驗證與授權過程即可。刷新時,如果訪問令牌有效,用戶便無需再次輸入登錄信息,一切都會在后臺進行,訪問令牌會自動刷新,延遲有效期60天。然而,對于大多數 Web 應用,存在一個常見情況:后臺刷新的一個基本前提,是用戶已經登錄了他們的 LinkedIn 賬號,而對于 App 中的內部 Web 視圖,這一條件無法滿足。因此,在訪問令牌快要到期之前,你很可能要讓用戶再走一遍登錄流程。想要了解更多信息,可以點擊此處,查看“刷新訪問令牌”一節。好了,說再見的時候到了。筆者希望本教程對你有所幫助,并成功向 LinkedIn 發送經過授權的請求。

作為參考,你可以從 GitHub 下載本案例完整的 Xcode 項目文件

OneAPM Mobile Insight 以真實用戶體驗為度量標準進行 Crash 分析,監控網絡請求及網絡錯誤,提升用戶留存。訪問 OneAPM 官方網站感受更多應用性能優化體驗,想閱讀更多技術文章,請訪問 OneAPM 官方技術博客

本文轉自 OneAPM 官方博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容