创建自定义令牌

Firebase 允许使用安全的 JSON Web 令牌 (JWT) 对用户或设备进行身份验证,让您对身份验证流程拥有完全的掌控力。您可以在自己的服务器上生成这些令牌,并将它们传递回客户端设备,然后通过 signInWithCustomToken() 方法使用这些令牌进行身份验证。

为了实现上述目标,您必须创建一个接受登录凭据(如用户名和密码)并在凭据有效时返回自定义 JWT 的服务器端点。这样,客户端设备便可使用从您的服务器返回的这个自定义 JWT 进行 Firebase 身份验证(iOS+AndroidWeb)。通过验证的身份会被用于访问其他 Firebase 服务(例如 Firebase Realtime DatabaseCloud Storage)。此外,Realtime Database Security Rules中的 auth 对象和 Cloud Storage Security Rules中的 request.auth 对象包含 JWT 内容。

您可以使用 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 管理的环境中使用,此方法将使用指定服务账号的密钥对令牌签名。 不过,它会使用远程 Web 服务,您可能需要通过 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 服务来对自定义令牌签名。App Identity 服务使用 Google App Engine 为您的应用预配的服务账号对数据签名。

  • 如果您的代码部署在其他代管式环境(例如 Google Cloud Functions、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 文件。

服务账号 ID 可在 Google Cloud 控制台中找到,也可以在下载的服务账号 JSON 文件的 client_email 字段中找到。服务账号 ID 是格式如下的电子邮件地址:<client-id>@<project-id>.iam.gserviceaccount.com。这些 ID 用于唯一标识 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 不是敏感信息,因此公开这些 ID 无关紧要。不过,如需使用指定的服务账号对自定义令牌签名,Firebase Admin SDK 必须调用远程服务。 此外,您还必须确保 Admin SDK 用于进行此调用的服务账号(通常为 {project-name}@appspot.gserviceaccount.com)具有 iam.serviceAccounts.signBlob 权限。 如需了解详情,请参阅下面的问题排查部分。

使用 Firebase Admin SDK 创建自定义令牌

Firebase Admin SDK 内置了创建自定义令牌的方法。您至少需要提供一个 uid,它可以是任何可唯一标识要进行身份验证的用户或设备的字符串。这些令牌会在 1 小时后过期。

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);

Web

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

Web

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())时一样,系统会使用用户的 uid 填充 Realtime Database Security Rules中的 auth 对象和 Cloud Storage Security Rules中的 request.auth 对象。在这种情况下,该 uid 将是在生成自定义令牌时您指定的那个。

数据库规则

{
  "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 Realtime Database) 或 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 晚最多 3600 秒
注意:这仅会控制自定义令牌本身的过期时间。但是,一旦您使用 signInWithCustomToken() 让用户登录,他们将一直在设备上保持登录状态,直到其会话失效或用户退出账号为止。
uid 已登录用户的唯一标识符(必须是长度为 1 到 128 个字符的字符串,含 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

使用 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”按钮为您的项目启用此 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}.

解决此问题的最简单方法是向相关服务账号(通常为 {project-name}@appspot.gserviceaccount.com)授予“Service Account Token Creator”IAM 角色:

  1. 打开 Google Cloud 控制台中的 IAM 和管理页面。
  2. 选择您的项目,然后点击“继续”。
  3. 点击与要更新的服务账号对应的修改图标。
  4. 点击“添加其他角色”。
  5. 在搜索过滤器中输入“Service Account Token Creator”,然后从结果中选择它。
  6. 点击“保存”确认授予此角色。

如需详细了解此过程,或了解如何使用 gcloud 命令行工具更新角色,请参阅 IAM 文档

无法确定服务账号

如果您收到类似如下内容的错误消息,则表明 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。