创建自定义登录页面

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

如果您并不需要完全自定义界面, IAP 为您托管登录页面, 或使用 FirebaseUI 以获得更流畅的体验。

概览

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

  1. 启用外部身份。在设置期间选择我将提供自己的界面选项
  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 v2.0.0 或更高版本,请完成以下操作:

  • package.json 文件中的 firebase 版本更新为 v9.8.3 及更高版本。
  • 按如下所示更新 firebase 导入语句:
  // 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() 以返回 Auth 实例, 与提供的 API 密钥和租户 ID 相对应。如果没有租户 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();

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

后续步骤