建立自訂權杖

Firebase 可讓您使用安全的 JSON Web Token (JWT) 驗證使用者或裝置,藉此完全掌控驗證。您可以在伺服器上產生這些權杖,將這些權杖傳回用戶端裝置,然後透過 signInWithCustomToken() 方法使用這些權杖進行驗證。

為此,您必須建立接受登入憑證 (例如使用者名稱和密碼) 的伺服器端點;如果憑證有效,則會傳回自訂 JWT。伺服器傳回的自訂 JWT 可供用戶端裝置使用,向 Firebase 進行驗證 (iOS+Android網頁)。驗證完成後,系統就會使用這個身分存取其他 Firebase 服務,例如 Firebase 即時資料庫和 Cloud Storage。此外,JWT 的內容會顯示在即時資料庫安全性規則auth 物件,以及 Cloud Storage 安全性規則中的 request.auth 物件中。

您可以透過 Firebase Admin SDK 建立自訂權杖。如果您的伺服器是以 Firebase 不原生支援的語言編寫,則可以使用第三方 JWT 程式庫。

事前準備

自訂權杖是已簽署的 JWT,其中簽署用的私密金鑰屬於 Google 服務帳戶。您可以透過下列幾種方式,指定 Firebase Admin SDK 應使用哪個 Google 服務帳戶簽署自訂憑證:

  • 使用服務帳戶 JSON 檔案 -- 此方法可在任何環境中使用,但您必須封裝服務帳戶 JSON 檔案與程式碼。必須特別留意,確保服務帳戶 JSON 檔案未對外部方公開。
  • 讓 Admin SDK 探索服務帳戶 - 這個方法可用於 Google 管理的環境,例如 Google Cloud Functions 和 App Engine。您可能需要透過 Google Cloud 控制台設定其他權限。
  • 使用服務帳戶 ID - 在 Google 代管的環境中使用時,這個方法會使用指定服務帳戶的金鑰簽署權杖。 然而,它會使用遠端網路服務,因此您可能必須透過 Google Cloud 控制台為這個服務帳戶設定其他權限。

使用服務帳戶 JSON 檔案

服務帳戶 JSON 檔案包含服務帳戶對應的所有資訊 (包括 RSA 私密金鑰)。您可以從 Firebase 主控台下載。請參閱「Admin SDK 設定操作說明」,進一步瞭解如何使用服務帳戶 JSON 檔案初始化 Admin SDK。

這種初始化方法適用於各種 Admin SDK 部署。而且 Admin SDK 也能在本機建立及簽署自訂權杖,不必執行任何遠端 API 呼叫。這種方法的主要缺點是,您必須封裝服務帳戶 JSON 檔案與程式碼。另請注意,服務帳戶 JSON 檔案中的私密金鑰屬於機密資訊,請務必特別留意,妥善保管這些資訊。具體而言,請勿將服務帳戶 JSON 檔案加入公開版本管控。

讓 Admin SDK 探索服務帳戶

如果您的程式碼部署於 Google 代管的環境,Admin SDK 可以嘗試自動探索簽署自訂權杖的方式:

  • 如果您的程式碼是部署在 Java、Python 或 Go 的 App Engine 標準環境中,Admin SDK 即可使用該環境中的應用程式身分識別服務簽署自訂權杖。App Identity 服務會使用 Google App Engine 為您的應用程式佈建的服務帳戶簽署資料。

  • 如果您的程式碼部署於其他代管環境 (例如 Google Cloud 函式、Google Compute Engine),Firebase Admin SDK 可自動從本機中繼資料伺服器探索服務帳戶 ID 字串。接著,探索到的服務帳戶 ID 就會與 IAM 服務搭配使用,從遠端簽署權杖。

如要使用這些簽署方法,請使用 Google 應用程式預設憑證初始化 SDK,且不要指定服務帳戶 ID 字串:

Node.js

initializeApp();

Java

FirebaseApp.initializeApp();

Python

default_app = firebase_admin.initialize_app()

Go

app, err := firebase.NewApp(context.Background(), nil)
if err != nil {
	log.Fatalf("error initializing app: %v\n", err)
}

C#

FirebaseApp.Create();

如要在本機測試相同的程式碼,請下載服務帳戶 JSON 檔案,並將 GOOGLE_APPLICATION_CREDENTIALS 環境變數設為指向該檔案。

如果 Firebase Admin SDK 必須探索服務帳戶 ID 字串,則會在程式碼首次建立自訂權杖時。系統會快取結果,並在後續的權杖簽署作業中重複使用。自動探索的服務帳戶 ID 通常是 Google Cloud 提供的其中一個預設服務帳戶:

自動探索服務帳戶 ID 和明確指定的服務帳戶 ID 一樣,都必須具有 iam.serviceAccounts.signBlob 權限,才能建立自訂權杖。您可能需要使用 Google Cloud 控制台的 IAM 與管理員區段,授予預設服務帳戶必要權限。詳情請參閱下方的疑難排解一節。

使用服務帳戶 ID

為了維持應用程式不同部分之間的一致性,您可以指定服務帳戶 ID;在 Google 代管的環境中執行時,這個帳戶的金鑰將用來簽署權杖。如此一來,IAM 政策將變得更簡單安全,也不必在程式碼中加入服務帳戶 JSON 檔案。

您可以在 Google Cloud 控制台或下載服務帳戶 JSON 檔案的 client_email 欄位中找到服務帳戶 ID。服務帳戶 ID 是採用下列格式的電子郵件地址:<client-id>@<project-id>.iam.gserviceaccount.com。專門用於識別 Firebase 和 Google Cloud 專案中的服務帳戶。

如要使用獨立的服務帳戶 ID 建立自訂權杖,請初始化 SDK,如下所示:

Node.js

initializeApp({
  serviceAccountId: 'my-client-id@my-project-id.iam.gserviceaccount.com',
});

Java

FirebaseOptions options = FirebaseOptions.builder()
    .setCredentials(GoogleCredentials.getApplicationDefault())
    .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com")
    .build();
FirebaseApp.initializeApp(options);

Python

options = {
    'serviceAccountId': 'my-client-id@my-project-id.iam.gserviceaccount.com',
}
firebase_admin.initialize_app(options=options)

Go

conf := &firebase.Config{
	ServiceAccountID: "my-client-id@my-project-id.iam.gserviceaccount.com",
}
app, err := firebase.NewApp(context.Background(), conf)
if err != nil {
	log.Fatalf("error initializing app: %v\n", err)
}

C#

FirebaseApp.Create(new AppOptions()
{
    Credential = GoogleCredential.GetApplicationDefault(),
    ServiceAccountId = "my-client-id@my-project-id.iam.gserviceaccount.com",
});

服務帳戶 ID 不是機密資訊,因此資料外洩會有所影響。不過,如要使用指定的服務帳戶簽署自訂權杖,Firebase Admin SDK 必須叫用遠端服務。此外,您也必須確認 Admin SDK 用來進行此呼叫的服務帳戶 (通常是 {project-name}@appspot.gserviceaccount.com) 具備 iam.serviceAccounts.signBlob 權限。詳情請參閱下方的疑難排解一節。

使用 Firebase Admin SDK 建立自訂權杖

Firebase Admin SDK 內建可建立自訂權杖的方法,您至少需要提供 uid,這可以是任何字串,但此屬性應專門用於識別您要驗證的使用者或裝置。這些憑證會在一小時後過期。

Node.js

const uid = 'some-uid';

getAuth()
  .createCustomToken(uid)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

Java

String uid = "some-uid";

String customToken = FirebaseAuth.getInstance().createCustomToken(uid);
// Send token back to client

Python

uid = 'some-uid'

custom_token = auth.create_custom_token(uid)

Go

client, err := app.Auth(context.Background())
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

token, err := client.CustomToken(ctx, "some-uid")
if err != nil {
	log.Fatalf("error minting custom token: %v\n", err)
}

log.Printf("Got custom token: %v\n", token)

C#

var uid = "some-uid";

string customToken = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(uid);
// Send token back to client

您也可以選擇指定要納入自訂符記的其他憑證附加資訊。舉例來說,在下方自訂權杖已新增 premiumAccount 欄位,可在安全性規則中的 auth / request.auth 物件中找到:

Node.js

const userId = 'some-uid';
const additionalClaims = {
  premiumAccount: true,
};

getAuth()
  .createCustomToken(userId, additionalClaims)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

Java

String uid = "some-uid";
Map<String, Object> additionalClaims = new HashMap<String, Object>();
additionalClaims.put("premiumAccount", true);

String customToken = FirebaseAuth.getInstance()
    .createCustomToken(uid, additionalClaims);
// Send token back to client

Python

uid = 'some-uid'
additional_claims = {
    'premiumAccount': True
}

custom_token = auth.create_custom_token(uid, additional_claims)

Go

client, err := app.Auth(context.Background())
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

claims := map[string]interface{}{
	"premiumAccount": true,
}

token, err := client.CustomTokenWithClaims(ctx, "some-uid", claims)
if err != nil {
	log.Fatalf("error minting custom token: %v\n", err)
}

log.Printf("Got custom token: %v\n", token)

C#

var uid = "some-uid";
var additionalClaims = new Dictionary<string, object>()
{
    { "premiumAccount", true },
};

string customToken = await FirebaseAuth.DefaultInstance
    .CreateCustomTokenAsync(uid, additionalClaims);
// Send token back to client

保留的自訂權杖名稱

在用戶端使用自訂權杖登入

建立自訂權杖後,您應該將其傳送至用戶端應用程式。用戶端應用程式會呼叫 signInWithCustomToken(),以自訂權杖進行驗證:

iOS+

Objective-C
[[FIRAuth auth] signInWithCustomToken:customToken
                           completion:^(FIRAuthDataResult * _Nullable authResult,
                                        NSError * _Nullable error) {
  // ...
}];
Swift
Auth.auth().signIn(withCustomToken: customToken ?? "") { user, error in
  // ...
}

Android

mAuth.signInWithCustomToken(mCustomToken)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    // Sign in success, update UI with the signed-in user's information
                    Log.d(TAG, "signInWithCustomToken:success");
                    FirebaseUser user = mAuth.getCurrentUser();
                    updateUI(user);
                } else {
                    // If sign in fails, display a message to the user.
                    Log.w(TAG, "signInWithCustomToken:failure", task.getException());
                    Toast.makeText(CustomAuthActivity.this, "Authentication failed.",
                            Toast.LENGTH_SHORT).show();
                    updateUI(null);
                }
            }
        });

Unity

auth.SignInWithCustomTokenAsync(custom_token).ContinueWith(task => {
  if (task.IsCanceled) {
    Debug.LogError("SignInWithCustomTokenAsync was canceled.");
    return;
  }
  if (task.IsFaulted) {
    Debug.LogError("SignInWithCustomTokenAsync encountered an error: " + task.Exception);
    return;
  }

  Firebase.Auth.AuthResult result = task.Result;
  Debug.LogFormat("User signed in successfully: {0} ({1})",
      result.User.DisplayName, result.User.UserId);
});

C++

firebase::Future<firebase::auth::AuthResult> result =
    auth->SignInWithCustomToken(custom_token);

網路命名空間 API

firebase.auth().signInWithCustomToken(token)
  .then((userCredential) => {
    // Signed in
    var user = userCredential.user;
    // ...
  })
  .catch((error) => {
    var errorCode = error.code;
    var errorMessage = error.message;
    // ...
  });

網頁模組 API

import { getAuth, signInWithCustomToken } from "firebase/auth";

const auth = getAuth();
signInWithCustomToken(auth, token)
  .then((userCredential) => {
    // Signed in
    const user = userCredential.user;
    // ...
  })
  .catch((error) => {
    const errorCode = error.code;
    const errorMessage = error.message;
    // ...
  });

如果驗證成功,使用者現在會用自訂權杖所含 uid 指定的帳戶登入用戶端應用程式。如果該帳戶先前不存在,系統會為該使用者建立記錄。

與其他登入方法 (例如 signInWithEmailAndPassword()signInWithCredential()) 的處理方式與即時資料庫安全性規則中的 auth 物件相同,而 Cloud Storage 安全性規則中的 request.auth 物件將填入使用者的 uid。在這種情況下,uid 會是您在產生自訂權杖時指定的 ID。

資料庫規則

{
  "rules": {
    "adminContent": {
      ".read": "auth.uid === 'some-uid'"
    }
  }
}

儲存空間規則

service firebase.storage {
  match /b/<your-firebase-storage-bucket>/o {
    match /adminContent/{filename} {
      allow read, write: if request.auth != null && request.auth.uid == "some-uid";
    }
  }
}

如果自訂權杖包含其他憑證附加資訊,您可以在規則中的 auth.token (Firebase 即時資料庫) 或 request.auth.token (Cloud Storage) 物件中參照這些憑證附加資訊:

資料庫規則

{
  "rules": {
    "premiumContent": {
      ".read": "auth.token.premiumAccount === true"
    }
  }
}

儲存空間規則

service firebase.storage {
  match /b/<your-firebase-storage-bucket>/o {
    match /premiumContent/{filename} {
      allow read, write: if request.auth.token.premiumAccount == true;
    }
  }
}

使用第三方 JWT 程式庫建立自訂權杖

如果後端使用的語言沒有官方 Firebase Admin SDK,您仍然可以手動建立自訂權杖。首先,請針對您的語言尋找第三方 JWT 程式庫。接著,使用這個 JWT 程式庫建立含有以下憑證詞的 JWT:

自訂權杖憑證附加資訊
alg 演算法 "RS256"
iss 核發單位 專案的服務帳戶電子郵件地址
sub 主旨 專案的服務帳戶電子郵件地址
aud 目標對象 "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
iat 核發時間 目前時間 (從 UNIX 紀元起算,以秒為單位)
exp 到期時間 權杖到期時間 (從 UNIX 紀元起算,以秒為單位)。最多可以比 iat 晚 3,600 秒
注意:這項設定只能控制自訂權杖本身的到期時間。不過,當您使用 signInWithCustomToken() 登入使用者後,裝置會保持登入狀態,直到工作階段失效或使用者登出為止。
uid 已登入使用者的專屬 ID 必須為字串,長度介於 1 至 128 個字元 (含首尾)。較短的 uid 則可改善效能。
claims (非必要) 要加入安全性規則 auth / request.auth 變數的選用自訂憑證附加資訊

以下提供幾個實作範例,說明如何以 Firebase Admin SDK 不支援的各種語言建立自訂權杖:

PHP

使用 php-jwt

// Requires: composer require firebase/php-jwt
use Firebase\JWT\JWT;

// Get your service account's email address and private key from the JSON key file
$service_account_email = "abc-123@a-b-c-123.iam.gserviceaccount.com";
$private_key = "-----BEGIN PRIVATE KEY-----...";

function create_custom_token($uid, $is_premium_account) {
  global $service_account_email, $private_key;

  $now_seconds = time();
  $payload = array(
    "iss" => $service_account_email,
    "sub" => $service_account_email,
    "aud" => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
    "iat" => $now_seconds,
    "exp" => $now_seconds+(60*60),  // Maximum expiration time is one hour
    "uid" => $uid,
    "claims" => array(
      "premium_account" => $is_premium_account
    )
  );
  return JWT::encode($payload, $private_key, "RS256");
}

小茹

使用 ruby-jwt

require "jwt"

# Get your service account's email address and private key from the JSON key file
$service_account_email = "service-account@my-project-abc123.iam.gserviceaccount.com"
$private_key = OpenSSL::PKey::RSA.new "-----BEGIN PRIVATE KEY-----\n..."

def create_custom_token(uid, is_premium_account)
  now_seconds = Time.now.to_i
  payload = {:iss => $service_account_email,
             :sub => $service_account_email,
             :aud => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
             :iat => now_seconds,
             :exp => now_seconds+(60*60), # Maximum expiration time is one hour
             :uid => uid,
             :claims => {:premium_account => is_premium_account}}
  JWT.encode payload, $private_key, "RS256"
end

建立自訂權杖後,請將其傳送至用戶端應用程式,以在 Firebase 中進行驗證。操作方法請參閱上方的程式碼範例。

疑難排解

本節將概述開發人員建立自訂符記時可能遇到的一些常見問題,以及解決方法。

未啟用 IAM API

如果您為簽署權杖指定服務帳戶 ID,可能會收到類似下列內容的錯誤:

Identity and Access Management (IAM) API has not been used in project
1234567890 before or it is disabled. Enable it by visiting
https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=1234567890
then retry. If you enabled this API recently, wait a few minutes for the action
to propagate to our systems and retry.

Firebase Admin SDK 會使用 IAM API 來簽署符記。這個錯誤表示您的 Firebase 專案目前尚未啟用 IAM API。在網路瀏覽器中開啟錯誤訊息中的連結,然後按一下「啟用 API」按鈕,為專案啟用該連結。

服務帳戶沒有必要權限

如果 Firebase Admin SDK 執行的服務帳戶沒有 iam.serviceAccounts.signBlob 權限,可能會收到如下所示的錯誤訊息:

Permission iam.serviceAccounts.signBlob is required to perform this operation
on service account projects/-/serviceAccounts/{your-service-account-id}.

解決這個問題最簡單的方法,就是將「服務帳戶權杖建立者」IAM 角色授予相關服務帳戶,通常是 {project-name}@appspot.gserviceaccount.com

  1. 在 Google Cloud 控制台中開啟「IAM and admin」(IAM 與管理) 頁面。
  2. 選取您的專案並點選 [繼續]。
  3. 按一下與您要更新的服務帳戶對應的編輯圖示。
  4. 按一下 [新增其他角色]。
  5. 在搜尋篩選器中輸入「Service Account Token Creator」,然後從結果選取該權杖。
  6. 點選「儲存」確認授予的角色。

如要進一步瞭解這項程序,請參閱身分與存取權管理說明文件,或是瞭解如何使用 gcloud 指令列工具更新角色。

無法判斷服務帳戶

如果您看到類似以下的錯誤訊息,表示 Firebase Admin SDK 尚未正確初始化。

Failed to determine service account ID. Initialize the SDK with service account
credentials or specify a service account ID with iam.serviceAccounts.signBlob
permission.

如果您使用 SDK 自動探索服務帳戶 ID,請確保程式碼部署於含有中繼資料伺服器的代管 Google 環境。否則,請務必在 SDK 初始化時指定服務帳戶 JSON 檔案或服務帳戶 ID。