创建自定义登录页面

本文介绍如何使用外部身份和 IAP 构建自己的身份验证页面。自行构建此页面可让您完全控制身份验证流程和用户体验。

如果您不需要完全自定义界面,则可以让 IAP 为您托管登录页面,或者使用 FirebaseUI 以获得更精简的体验。

概览

如需构建您自己的身份验证页面,请按以下步骤操作:

  1. 启用外部身份。在设置过程中,选择 I'll provide my own UI option(我将提供自己的界面选项)。
  2. 安装 gcip-iap
  3. 通过实现 AuthenticationHandler 接口来配置界面。您的身份验证页面必须处理以下场景:
    • 租户选择
    • 用户授权
    • 用户登录
    • 错误处理
  4. 可选:使用其他功能自定义身份验证页面,例如进度条、退出页面和用户处理。
  5. 测试界面

安装 gcip-iap 库

如需安装 gcip-iap 库,请运行以下命令:

npm install gcip-iap --save

gcip-iap NPM 模块可以抽象化应用、IAP 和 Identity Platform 之间的通信。这样,您就可以自定义整个身份验证流程,而无需管理界面和 IAP 之间的底层交换。

为您的 SDK 版本使用正确的导入:

gcip-iap v0.1.4 或更早版本

// Import Firebase/GCIP dependencies. These are installed on npm install.
import * as firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';

gcip-iap v1.0.0 至 v1.1.0

从版本 v1.0.0 开始,gcip-iap 需要 firebase v9 对等依赖项或更高版本。 如果您要迁移到 gcip-iap 1.0.0 版或更高版本,请执行以下操作:

  • package.json 文件中的 firebase 版本更新为 9.6.0 版及更高版本。
  • 更新 firebase import 语句,如下所示:
// Import Firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import the gcip-iap module.
import * as ciap from 'gcip-iap';

无需对代码进行额外的更改。

gcip-iap 2.0.0 版

从版本 v2.0.0 开始,gcip-iap 要求使用模块化 SDK 格式重写自定义界面应用。如果您要迁移到 gcip-iap 2.0.0 版或更高版本,请完成以下步骤:

  • package.json 文件中的 firebase 版本更新为 v9.8.3 及更高版本。
  • 更新 firebase import 语句,如下所示:
  // Import Firebase modules.
  import { initializeApp } from 'firebase/app';
  import { getAuth, GoogleAuthProvider } 'firebase/auth';
  // Import the gcip-iap module.
  import * as ciap from 'gcip-iap';

配置界面

如需配置界面,请创建一个实现 AuthenticationHandler 接口的自定义类:

interface AuthenticationHandler {
  languageCode?: string | null;
  getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
  startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
  selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
  completeSignOut(): Promise<void>;
  processUser?(user: User): Promise<User>;
  showProgressBar?(): void;
  hideProgressBar?(): void;
  handleError?(error: Error | CIAPError): void;
}

在身份验证期间,该库会自动调用 AuthenticationHandler 的方法。

选择租户

如需选择租户,请实现 selectTenant()。您可以实现此方法以程序化地选择租户,也可以显示界面以便用户自行选择一个租户。

无论是哪种情况,库都会使用返回的 SelectedTenantInfo 对象来完成身份验证流程。它包含所选租户的 ID、所有提供商 ID 以及用户输入的电子邮件地址。

如果您的项目中有多个租户,则必须先选择一个租户,然后才能对用户进行身份验证。如果您只有一个租户,或者使用的是项目级身份验证,则无需实现 selectTenant()

IAP 支持的提供商与 Identity Platform 相同,例如:

  • 电子邮件地址和密码
  • OAuth(Google、Facebook、Twitter、GitHub、Microsoft 等)
  • SAML
  • OIDC
  • 电话号码
  • 自定义
  • 匿名

多租户不支持电话号码、自定义和匿名身份验证类型。

以编程方式选择租户

如需以编程方式选择租户,请利用当前上下文。Authentication 类包含 getOriginalURL(),它可返回用户在身份验证之前访问的网址。

使用此方法可从关联租户列表中找到匹配项:

// Select provider programmatically.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    auth.getOriginalURL()
      .then((originalUrl) => {
        resolve({
          tenantId: getMatchingTenantBasedOnVisitedUrl(originalUrl),
          // If associated provider IDs can also be determined,
          // populate this list.
          providerIds: [],
        });
      })
      .catch(reject);
  });
}

允许用户选择租户

如需允许用户选择租户,请显示租户列表并让用户选择一个,或者要求用户输入其电子邮件地址,然后根据网域找到匹配项:

// Select provider by showing UI.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    renderSelectTenant(
        tenantIds,
        // On tenant selection.
        (selectedTenantId) => {
          resolve({
            tenantId: selectedTenantId,
            // If associated provider IDs can also be determined,
            // populate this list.
            providerIds: [],
            // If email is available, populate this field too.
            email: undefined,
          });
        });
  });
}

对用户进行身份验证

在拥有提供方后,请实现 getAuth() 以返回与提供的 API 密钥和租户 ID 对应的 Auth 实例。如果未提供租户 ID,请使用项目级身份提供方。

getAuth() 会跟踪与提供的配置对应的用户的存储位置。它还使得可以静默刷新先前已认证的用户的 Identity Platform ID 令牌,而无需用户重新输入其凭据。

如果您使用具有不同租户的多个 IAP 资源,我们建议您为每个资源使用唯一的身份验证实例。这允许具有不同配置的多个资源使用同一身份验证页面。它还允许多个用户同时登录而无需退出前一个用户。

以下是一个有关如何实现 getAuth() 的示例:

gcip-iap v1.0.0

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = firebase.app(tenantId || undefined).auth();
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = firebase.initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = app.auth();
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

gcip-iap v2.0.0

import {initializeApp, getApp} from 'firebase/app';
import {getAuth} from 'firebase/auth';

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = getAuth(getApp(tenantId || undefined));
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = getAuth(app);
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

让用户登录

如需处理登录操作,请实现 startSignIn(),向用户显示进行身份验证的界面,然后在操作完成后为已登录的用户返回 UserCredential

在多租户环境中,您可以从 SelectedTenantInfo 确定可用的身份验证方法(如果已提供)。此变量包含 selectTenant() 返回的相同信息。

以下示例展示了使用电子邮件地址和密码的现有用户的 startSignIn() 实现:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
      auth.signInWithEmailAndPassword(email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

gcip-iap v2.0.0

import {signInWithEmailAndPassword} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
        signInWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

您还可以用弹出式窗口或重定向通过联盟提供商(例如 SAML 或 OIDC)让用户登录:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in may have already been determined from the
    // selectedTenantInfo object.
    const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    auth.signInWithRedirect(provider);
  });
}

gcip-iap v2.0.0

import {signInWithPopup, SAMLAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in might have already been determined from the
    // selectedTenantInfo object.
    const provider = new SAMLAuthProvider('saml.myProvider');
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    signInWithRedirect(auth, provider);
  });
}

一些 OAuth 提供商支持传递登录提示以进行登录:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new firebase.auth.OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

gcip-iap v2.0.0

import {signInWithPopup, OAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign in or sign up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

如需了解详情,请参阅使用多租户进行身份验证

处理错误

如需向用户显示错误消息或尝试从网络超时等错误中恢复,请实现 handleError()

以下示例实现了 handleError()

handleError(error) {
  showAlert({
    code: error.code,
    message: error.message,
    // Whether to show the retry button. This is only available if the error is
    // recoverable via retrial.
    retry: !!error.retry,
  });
  // When user clicks retry, call error.retry();
  $('.alert-link').on('click', (e) => {
    error.retry();
    e.preventDefault();
    return false;
  });
}

下表列出了可以返回的 IAP 特定错误代码。Identity Platform 也可能会返回错误;请参阅 firebase.auth.Auth 的文档。

错误代码 说明
invalid-argument 客户端指定了无效参数。
failed-precondition 请求无法在当前系统状态下执行。
out-of-range 客户端指定了无效范围。
unauthenticated 由于缺少、无效或过期的 OAuth 令牌,请求未通过身份验证。
permission-denied 客户端没有足够的权限,或者界面托管在未经授权的网域中。
not-found 未找到指定的资源。
aborted 并发冲突,例如读取/修改/写入冲突。
already-exists 客户端尝试创建的资源已存在。
resource-exhausted 资源配额不足或达到了速率限制。
cancelled 请求被客户端取消。
data-loss 出现了不可恢复的数据丢失或数据损坏问题。
unknown 出现未知的服务器错误。
internal 内部服务器错误。
not-implemented API 方法未通过服务器实现。
unavailable 服务不可用。
restart-process 重新访问将您重定向到此页面的网址,以重新启动身份验证流程。
deadline-exceeded 已超过请求时限。
authentication-uri-fail 未生成身份验证 URI。
gcip-token-invalid 提供了无效的 GCIP ID 令牌。
gcip-redirect-invalid 无效的重定向网址。
get-project-mapping-fail 未获取项目 ID。
gcip-id-token-encryption-error GCIP ID 令牌加密错误。
gcip-id-token-decryption-error GCIP ID 令牌解密错误。
gcip-id-token-unescape-error Web 安全 base64 unescape 失败。
resource-missing-gcip-sign-in-url 指定的 IAP 资源缺少 GCIP 身份验证网址。

自定义界面

您可以使用进度条和退出页面等可选功能来自定义身份验证页面。

显示进度界面

如需在 gcip-iap 模块执行长时间运行的网络任务时向用户显示自定义进度界面,请实现 showProgressBar()hideProgressBar()

让用户退出账号

在某些情况下,您可能希望允许用户从共享相同身份验证网址的所有当前会话中退出。

用户退出后,可能没有可重定向回的网址。 当用户从与登录页面关联的所有租户中退出账号时,通常就会发生这种情况。在这种情况下,请实现 completeSignOut() 以显示一条消息,指明用户已成功退出登录。如果您不实现此方法,当用户退出帐号时,系统会显示空白页面。

正在处理用户

如需在重定向到 IAP 资源之前修改已登录用户,请实现 processUser()

您可以使用此方法执行以下操作:

  • 链接到其他提供商。
  • 更新用户的个人资料。
  • 注册后要求用户提供其他数据。
  • 调用 signInWithRedirect() 之后,处理 getRedirectResult() 返回的 OAuth 访问令牌。

以下是实现 processUser() 的示例:

gcip-iap v1.0.0

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

gcip-iap v2.0.0

import {getRedirectResult} from 'firebase/auth';

processUser(user) {
  return getRedirectResult(lastAuthUsed).then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

如果您希望对用户所做的任何更改反映在 IAP 传播到应用的 ID 令牌声明中,则必须强制刷新令牌:

gcip-iap v1.0.0

processUser(user) {
  return user.updateProfile({
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

gcip-iap v2.0.0

import {updateProfile} from 'firebase/auth';

processUser(user) {
  return updateProfile(user, {
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

测试界面

创建实现 AuthenticationHandler 的类后,您可以使用它来创建新的 Authentication 实例并启动该实例:

// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();

部署您的应用并转到身份验证页面。您应该会看到自定义登录界面。

后续步骤