如何通過 OAuth 2.0 使 iOS Apps 整合 LinkedIn 登入功能?

OneAPM官方技術部落格發表於2016-03-21

社交網路早已成為人們日常生活的一部分。其實,社交網路也是程式設計生活的一部分,大多數 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 官方部落格

相關文章