驗證 OAuth2 服務

驗證權杖邏輯圖表
圖 1.從 Android 客戶經理取得有效驗證權杖的程序。

為了安全地存取線上服務,使用者需要驗證服務,而他們需要提供身分證明。對於存取第三方服務的應用程式,安全性問題會更複雜。使用者不僅需要通過驗證才能存取服務,應用程式也必須取得授權,才能代表使用者採取行動。

處理第三方服務驗證的業界標準做法是 OAuth2 通訊協定。OAuth2 提供單一值 (稱為驗證權杖),代表使用者身分和應用程式授權,可代表使用者的身分採取行動。本課程示範如何連線至支援 OAuth2 的 Google 伺服器。雖然其中使用 Google 服務的範例,但實際介紹的技術也適用於任何正確支援 OAuth2 通訊協定的服務。

使用 OAuth2 的好處如下:

  • 取得使用者透過帳戶存取線上服務的權限。
  • 代表使用者為線上服務驗證。
  • 處理驗證錯誤。

收集資訊

如要開始使用 OAuth2,您需要瞭解所要存取服務的 API 專屬資訊:

  • 您要存取的服務網址。
  • 驗證範圍是字串,定義應用程式要求的特定存取權類型。舉例來說,Google Tasks 唯讀存取權的驗證範圍為 View your tasks,而 Google Tasks 讀取/寫入存取權的驗證範圍為 Manage your tasks
  • 用戶端 ID 和用戶端密鑰,為服務向服務識別您的應用程式的字串。您必須直接向服務擁有者取得這些字串。Google 有專門用來取得用戶端 ID 和密鑰的自助式系統。

要求網際網路權限

如果應用程式指定 Android 6.0 (API 級別 23) 以上版本,則 getAuthToken() 方法本身不需要任何權限。不過,如要對權杖執行作業,您必須將 INTERNET 權限新增至資訊清單檔案,如以下程式碼片段所示:

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

要求驗證權杖

如要取得權杖,請呼叫 AccountManager.getAuthToken()

注意: 由於某些帳戶作業可能會涉及網路通訊,因此大部分的 AccountManager 方法都是非同步的。也就是說,您需要以一系列回呼的形式實作,而非在單一函式中執行所有驗證作業。

下列程式碼片段說明如何使用一系列的回呼取得權杖:

Kotlin

val am: AccountManager = AccountManager.get(this)
val options = Bundle()

am.getAuthToken(
        myAccount_,                     // Account retrieved using getAccountsByType()
        "Manage your tasks",            // Auth scope
        options,                        // Authenticator-specific options
        this,                           // Your activity
        OnTokenAcquired(),              // Callback called when a token is successfully acquired
        Handler(OnError())              // Callback called if an error occurs
)

Java

AccountManager am = AccountManager.get(this);
Bundle options = new Bundle();

am.getAuthToken(
    myAccount_,                     // Account retrieved using getAccountsByType()
    "Manage your tasks",            // Auth scope
    options,                        // Authenticator-specific options
    this,                           // Your activity
    new OnTokenAcquired(),          // Callback called when a token is successfully acquired
    new Handler(new OnError()));    // Callback called if an error occurs

在這個範例中,OnTokenAcquired 是實作 AccountManagerCallback 的類別。AccountManager 會使用包含 BundleAccountManagerFutureOnTokenAcquired 上呼叫 run()。如果呼叫成功,權杖會位於 Bundle 中。

以下說明如何從 Bundle 取得權杖:

Kotlin

private class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        // Get the result of the operation from the AccountManagerFuture.
        val bundle: Bundle = result.getResult()

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        // Get the result of the operation from the AccountManagerFuture.
        Bundle bundle = result.getResult();

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        ...
    }
}

如果一切順利,BundleKEY_AUTHTOKEN 金鑰中會包含有效的權杖,這樣就大功告成了。

首次要求驗證權杖時可能會失敗,原因如下:

  • 裝置或網路發生錯誤,導致 AccountManager 失敗。
  • 使用者決定不將帳戶存取權授予您的應用程式。
  • 儲存的帳戶憑證不足以取得帳戶存取權。
  • 快取驗證權杖已過期。

應用程式可以簡單地處理前兩個情況,通常只需向使用者顯示錯誤訊息即可。如果網路停止運作,或是使用者決定不授予存取權,應用程式就無法提供足夠的功能。最後兩個情況會稍微複雜一點,因為擁有良好狀態的應用程式應會自動處理這類故障。

第三個失敗情況是憑證不足,會透過您在 AccountManagerCallback 中收到的 Bundle (上一個範例中的 OnTokenAcquired) 傳達。如果 BundleKEY_INTENT 金鑰中加入 Intent,驗證器就會指出必須先與使用者直接互動,才能提供有效權杖。

驗證器傳回 Intent 的原因可能有很多種。可能是使用者第一次登入這個帳戶。這可能是因為使用者的帳戶已過期,所以他們需要重新登入,或者儲存的憑證可能不正確。也許帳戶需要雙重驗證,或是需要啟用相機才能執行視網膜掃描。但這並不重要。如果您想取得有效的權杖,就必須關閉 Intent 才能取得。

Kotlin

private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
        if (launch != null) {
            startActivityForResult(launch, 0)
        }
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        ...
        Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
        if (launch != null) {
            startActivityForResult(launch, 0);
            return;
        }
    }
}

請注意,這個範例使用 startActivityForResult(),因此您可以在自己的活動中實作 onActivityResult(),藉此擷取 Intent 的結果。重要事項:如果您未從驗證者的回應 Intent 擷取結果,就無法判斷使用者是否已成功驗證。

如果結果是 RESULT_OK,驗證者已更新儲存的憑證,使其足以處理您要求的存取層級,建議您再次呼叫 AccountManager.getAuthToken() 以要求新的驗證權杖。

最後一個案例 (權杖已過期) 實際上並非 AccountManager 失敗。如要偵測權杖是否已過期,唯一的方法就是聯絡伺服器,這樣 AccountManager 要持續連線至網路來檢查其所有權杖的狀態,既浪費又昂貴的成本。因此,只有在類似應用程式嘗試使用驗證權杖存取線上服務時,系統才能偵測到這項失敗。

連線至線上服務

下例顯示如何連線至 Google 伺服器。由於 Google 使用產業標準 OAuth2 通訊協定來驗證要求,因此本文所述的技巧普遍適用。不過請注意,每部伺服器都不同。您可能會發現自己需要稍微調整這些操作說明,以考量到您遇到的問題。

Google API 會要求您為每個要求提供四個值:API 金鑰、用戶端 ID、用戶端密鑰和驗證金鑰。前三個來自 Google API 控制台網站最後是呼叫 AccountManager.getAuthToken() 取得的字串值。請將這些符記傳送至 Google 伺服器,做為 HTTP 要求的一部分。

Kotlin

val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
val conn = url.openConnection() as HttpURLConnection
conn.apply {
    addRequestProperty("client_id", your client id)
    addRequestProperty("client_secret", your client secret)
    setRequestProperty("Authorization", "OAuth $token")
}

Java

URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
URLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty("client_id", your client id);
conn.addRequestProperty("client_secret", your client secret);
conn.setRequestProperty("Authorization", "OAuth " + token);

如果要求傳回 HTTP 錯誤代碼 401,表示權杖已遭拒。如上一節所述,最常見的原因是權杖已過期。解決方法很簡單:請呼叫 AccountManager.invalidateAuthToken(),然後重複執行權杖取得程序。

由於過期的符記是常見的情況,修正方法十分簡單,因此許多應用程式會在要求權杖之前,直接假設權杖已過期。如果更新權杖是伺服器最便宜的作業,建議您在第一次呼叫 AccountManager.getAuthToken() 之前呼叫 AccountManager.invalidateAuthToken(),這樣就不需要要求驗證權杖兩次。