使用屏蔽函数扩展 Firebase Authentication


通过屏蔽函数,您可以执行自定义代码,以修改用户注册或登录应用的结果。例如,您可以禁止不符合特定标准的用户通过身份验证,或者首先对用户的信息进行更新,然后再将此信息返回到您的客户端应用。

准备工作

如需使用屏蔽函数,您必须将 Firebase 项目升级到 Firebase Authentication with Identity Platform。如果您尚未升级,请先升级。

了解屏蔽函数

您可以为三种事件注册屏蔽函数:

  • 在创建用户之前:在将新用户保存到 Firebase Authentication 数据库之前以及将令牌返回给客户端应用之前触发。

  • 在用户登录之前:在用户的凭据通过验证之后、Firebase Authentication 将 ID 令牌返回给客户端应用之前触发。如果您的应用使用多重身份验证,则该函数会在用户通过其第二重身份验证后触发。请注意,创建新用户也会触发这两种事件。

  • 在发送电子邮件之前(仅限 Node.js):在向用户发送电子邮件
    (例如登录或密码重置电子邮件)之前触发。

  • 在发送短信之前(仅限 Node.js):在向用户发送短信之前触发,适用于多重身份验证等情况。

使用屏蔽函数时,请谨记以下几点:

  • 您的函数必须在 7 秒内响应。经过 7 秒之后,Firebase Authentication 会返回错误,并且客户端操作会失败。

  • 200 以外的 HTTP 响应代码会传递到您的客户端应用。请确保您的客户端代码处理函数可能会返回的任何错误。

  • 函数会应用于项目中的所有用户,包括租户中包含的任何用户。Firebase Authentication 为您的函数提供用户相关信息(包括用户所属的任何租户),以便您可以做出相应的响应。

  • 如果将其他身份提供方关联至账号,则会再度触发所有已注册的 beforeUserSignedIn 函数。

  • 匿名和自定义身份验证不会触发屏蔽函数。

部署屏蔽函数

如需将自定义代码插入到用户身份验证流程中,请部署屏蔽函数。部署屏蔽函数后,自定义代码必须成功完成,身份验证和用户创建操作才能成功。

您可以像部署任何函数一样部署屏蔽函数。(如需了解详情,请参阅 Cloud Functions 使用入门页面)。总结:

  1. 编写一个用于处理所定位事件的函数。

    例如,如需开始使用,您可以向源代码添加以下免运维函数:

    import {
      beforeUserCreated,
    } from "firebase-functions/v2/identity";
    
    export const beforecreated = beforeUserCreated((event) => {
      // TODO
      return;
    });
    
    @identity_fn.before_user_created()
    def created_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
        return
    

    上面的示例省略了自定义身份验证逻辑的实现步骤。请参阅以下部分,了解实现屏蔽函数的方法以及特定示例的常见场景

  2. 使用 Firebase CLI 部署您的函数:

    firebase deploy --only functions
    

    每次更新函数时,您都必须重新部署函数。

获取用户和上下文信息

屏蔽事件提供了一个 AuthBlockingEvent 对象,其中包含有关用户登录的信息。在代码中使用这些值可确定是否允许某项操作继续。

该对象包含以下属性:

名称 说明 示例
locale 应用所用的语言区域。您可以使用客户端 SDK 设置语言区域,也可以通过在 REST API 中传递语言区域标头来进行设置。 frsv-SE
ipAddress 最终用户用于注册或登录的设备的 IP 地址。 114.14.200.1
userAgent 触发了屏蔽函数的用户代理。 Mozilla/5.0 (X11; Linux x86_64)
eventId 事件的唯一标识符。 rWsyPtolplG2TBFoOkkgyg
eventType 事件类型。提供有关事件名称(例如 beforeSignInbeforeCreate)以及所用登录方法(例如 Google 或电子邮件地址/密码)的信息。 providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType 始终为 USER USER
resource Firebase Authentication 项目或租户。 projects/project-id/tenants/tenant-id
timestamp 事件触发的时间,格式为 RFC 3339 字符串。 Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo 一个包含用户相关信息的对象。 AdditionalUserInfo
credential 一个包含用户凭据相关信息的对象。 AuthCredential

屏蔽注册或登录

如需屏蔽注册或登录尝试,请在您的函数中抛出 HttpsError。例如:

import { HttpsError } from "firebase-functions/v2/identity";

throw new HttpsError('invalid-argument');
raise https_fn.HttpsError(
    code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT)

您还可以指定自定义错误消息:

throw new HttpsError('permission-denied', 'Unauthorized request origin!');
raise https_fn.HttpsError(
    code=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
    message="Unauthorized request origin!"
)

以下示例展示了如何阻止不在特定网域内的用户注册您的应用:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from event.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (!user?.email?.includes('@acme.com')) {
    throw new HttpsError('invalid-argument', "Unauthorized email");
  }
});
# Block account creation with any non-acme email address.
@identity_fn.before_user_created()
def validatenewuser(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    # User data passed in from the CloudEvent.
    user = event.data

    # Only users of a specific domain can sign up.
    if user.email is None or "@acme.com" not in user.email:
        # Return None so that Firebase Auth rejects the account creation.
        raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                                  message="Unauthorized email")

无论您是使用默认消息还是自定义消息,Cloud Functions 都会封装相应错误,并将其作为内部错误返回给客户端。例如:

throw new HttpsError('invalid-argument', "Unauthorized email");
# Only users of a specific domain can sign up.
if user.email is None or "@acme.com" not in user.email:
    # Return None so that Firebase Auth rejects the account creation.
    raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                              message="Unauthorized email")

您的应用应捕获错误并相应地进行处理。例如:

import { getAuth, createUserWithEmailAndPassword } from 'firebase/auth';

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
const auth = getAuth();
try {
  const result = await createUserWithEmailAndPassword(auth)
  const idTokenResult = await result.user.getIdTokenResult();
  console.log(idTokenResult.claim.admin);
} catch(error) {
  if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
}

修改用户

您可以允许操作继续执行,而不必屏蔽注册或登录尝试,但应修改保存到 Firebase Authentication 数据库并返回给客户端的 User 对象。

如需修改用户,请从事件处理脚本返回包含要修改的字段的对象。您可以修改以下字段:

  • displayName
  • disabled
  • emailVerified
  • photoUrl
  • customClaims
  • sessionClaims(仅限 beforeUserSignedIn

sessionClaims 之外,所有修改后的字段都会保存到 Firebase Authentication 数据库,这意味着它们会包含在响应令牌中,并在多次用户会话之间继续留存。

以下示例展示了如何设置默认显示名称:

export const beforecreated = beforeUserCreated((event) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: event.data.displayName || 'Guest'
  };
});
@identity_fn.before_user_created()
def setdefaultname(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    return identity_fn.BeforeCreateResponse(
        # If no display name is provided, set it to "Guest".
        display_name=event.data.display_name if event.data.display_name is not None else "Guest")

如果您为 beforeUserCreatedbeforeUserSignedIn 注册了事件处理脚本,请注意 beforeUserSignedIn 会在 beforeUserCreated 之后执行。在 beforeUserCreated 中更新的用户字段会显示在 beforeUserSignedIn 中。如果您在两个事件处理脚本中均设置了 sessionClaims 以外的字段,则 beforeUserSignedIn 中设置的值会替换 beforeUserCreated 中设置的值。仅对于 sessionClaims,这些值会传播到当前会话的令牌声明中,而不会留存或存储在数据库中。

例如,如果设置了任何 sessionClaims,则 beforeUserSignedIn 将在响应中将其随同任何 beforeUserCreated 声明一起返回,并且会将两者合并在一起。合并后,如果某个 sessionClaims 键与 customClaims 中的某个键匹配,则匹配的 customClaims 将在令牌声明中被 sessionClaims 键所替换。但是,被覆盖的 customClaims 键仍将留存在数据库中,以供将来的请求使用。

支持的 OAuth 凭据和数据

您可以向屏蔽函数传递来自各个身份提供方的 OAuth 凭据和数据。下表显示了各身份提供方支持的凭据和数据:

身份提供商 ID 令牌 访问令牌 有效期 令牌 Secret 刷新令牌 登录声明
Google
Facebook
Twitter
GitHub
Microsoft
LinkedIn
Yahoo
Apple
SAML
OIDC

OAuth 令牌

如需在屏蔽函数中使用 ID 令牌、访问令牌或刷新令牌,您必须先选中 Firebase 控制台的屏蔽函数页面上的相应复选框。

使用 OAuth 凭据(例如 ID 令牌或访问令牌)直接登录时,任何身份提供方都不会返回刷新令牌。在这种情况下,系统会向屏蔽函数传递相同的客户端 OAuth 凭据。

以下部分介绍了每种身份提供方类型及其支持的凭据和数据。

常规 OIDC 提供方

当用户使用常规 OIDC 提供方登录时,系统将传递以下凭据:

  • ID 令牌:在选择了 id_token 流时提供。
  • 访问令牌:在选择了代码流时提供。请注意,目前仅通过 REST API 支持代码流。
  • 刷新令牌:在选择了 offline_access 范围时提供。

示例:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

当用户使用 Google 账号登录时,系统将传递以下凭据:

  • ID 令牌
  • 访问令牌
  • 刷新令牌:仅在请求以下自定义参数时提供:
    • access_type=offline
    • prompt=consent(如果用户之前同意并且未请求新的范围)

示例:

import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';

const auth = getAuth();
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
signInWithPopup(auth, provider);

详细了解 Google 刷新令牌

Facebook

当用户使用 Facebook 账号登录时,系统将传递以下凭据:

  • 访问令牌:返回可交换为另一个访问令牌的访问令牌。详细了解 Facebook 支持的不同类型的访问令牌,以及如何用这些令牌交换长期令牌

GitHub

当用户使用 GitHub 账号登录时,系统将传递以下凭据:

  • 访问令牌:除非撤销,否则该令牌不会过期。

Microsoft

当用户使用 Microsoft 账号登录时,系统将传递以下凭据:

  • ID 令牌
  • 访问令牌
  • 刷新令牌:如果选择了 offline_access 范围,则将此令牌传递给屏蔽函数。

示例:

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

const auth = getAuth();
const provider = new OAuthProvider('microsoft.com');
provider.addScope('offline_access');
signInWithPopup(auth, provider);

Yahoo

当用户使用 Yahoo 账号登录时,系统将传递以下凭据,但不包含任何自定义参数或范围:

  • ID 令牌
  • 访问令牌
  • 刷新令牌

LinkedIn

当用户使用 LinkedIn 账号登录时,系统将传递以下凭据:

  • 访问令牌

Apple

当用户使用 Apple 账号登录时,系统将传递以下凭据,但不包含任何自定义参数或范围:

  • ID 令牌
  • 访问令牌
  • 刷新令牌

常见方案

以下示例展示了屏蔽函数的一些常见使用场景:

仅允许从特定网域注册

以下示例展示了如何屏蔽不属于 example.com 网域成员的用户向您的应用注册:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (!user?.email?.includes('@example.com')) {
    throw new HttpsError(
      'invalid-argument', 'Unauthorized email');
  }
});
 @identity_fn.before_user_created()
   def validatenewuser(
       event: identity_fn.AuthBlockingEvent,
   ) -> identity_fn.BeforeCreateResponse | None:
       # User data passed in from the CloudEvent.
       user = event.data

       # Only users of a specific domain can sign up.
       if user.email is None or "@example.com" not in user.email:
           # Return None so that Firebase Auth rejects the account creation.
           raise https_fn.HttpsError(
               code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
               message="Unauthorized email",
           )

禁止使用未验证电子邮件地址的用户注册

以下示例展示了如何禁止用户使用未经验证的电子邮件地址向您的应用注册:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.email && !user.emailVerified) {
    throw new HttpsError(
      'invalid-argument', 'Unverified email');
  }
});
@identity_fn.before_user_created()
def requireverified(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if event.data.email is not None and not event.data.email_verified:
        raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                                  message="You must register using a trusted provider.")

将某些身份提供方电子邮件地址视为已验证

以下示例展示了如何将来自某些身份提供方的用户电子邮件地址视为已验证:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.email && !user.emailVerified && event.eventType.includes(':facebook.com')) {
    return {
      emailVerified: true,
    };
  }
});
@identity_fn.before_user_created()
def markverified(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if event.data.email is not None and "@facebook.com" in event.data.email:
        return identity_fn.BeforeSignInResponse(email_verified=True)

禁止通过特定 IP 地址登录

以下示例展示了如何禁止通过特定 IP 地址范围登录:

export const beforesignedin = beforeUserSignedIn((event) => {
  if (isSuspiciousIpAddress(event.ipAddress)) {
    throw new HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});
@identity_fn.before_user_signed_in()
def ipban(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None:
    if is_suspicious(event.ip_address):
        raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
                                  message="IP banned.")

设置自定义声明和会话声明

以下示例展示了如何设置自定义声明和会话声明:

export const beforecreated = beforeUserCreated((event) => {
    if (event.credential &&
        event.credential.claims &&
        event.credential.providerId === "saml.my-provider-id") {
        return {
            // Employee ID does not change so save in persistent claims (stored in
            // Auth DB).
            customClaims: {
                eid: event.credential.claims.employeeid,
            },
        };
    }
});

export const beforesignin = beforeUserSignedIn((event) => {
    if (event.credential &&
        event.credential.claims &&
        event.credential.providerId === "saml.my-provider-id") {
        return {
            // Copy role and groups to token claims. These will not be persisted.
            sessionClaims: {
                role: event.credential.claims.role,
                groups: event.credential.claims.groups,
            },
        };
    }
});
@identity_fn.before_user_created()
def setemployeeid(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if (event.credential is not None and event.credential.claims is not None and
            event.credential.provider_id == "saml.my-provider-id"):
        return identity_fn.BeforeCreateResponse(
            custom_claims={"eid": event.credential.claims["employeeid"]})


@identity_fn.before_user_signed_in()
def copyclaimstosession(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None:
    if (event.credential is not None and event.credential.claims is not None and
            event.credential.provider_id == "saml.my-provider-id"):
        return identity_fn.BeforeSignInResponse(session_claims={
            "role": event.credential.claims["role"],
            "groups": event.credential.claims["groups"]
        })

跟踪 IP 地址以监控可疑活动

您可以防范令牌盗用,方法是跟踪用户登录时所在的 IP 地址,并将该请求与后续请求的 IP 地址进行比较。如果请求看起来很可疑(例如,IP 地址来自不同的地理区域),您可以要求用户重新登录。

  1. 使用会话声明来跟踪用户登录时使用的 IP 地址:

    export const beforesignedin = beforeUserSignedIn((event) => {
      return {
        sessionClaims: {
          signInIpAddress: event.ipAddress,
        },
      };
    });
    
    @identity_fn.before_user_signed_in()
    def logip(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None:
        return identity_fn.BeforeSignInResponse(session_claims={"signInIpAddress": event.ip_address})
    
  2. 当用户尝试访问要求通过 Firebase Authentication 进行身份验证的资源时,将请求中的 IP 地址与用于登录的 IP 进行比较:

    app.post('/getRestrictedData', (req, res) => {
      // Get the ID token passed.
      const idToken = req.body.idToken;
      // Verify the ID token, check if revoked and decode its payload.
      admin.auth().verifyIdToken(idToken, true).then((claims) => {
        // Get request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session IP addresses. The current request timestamp and the
        // auth_time of the ID token can provide additional signals of abuse,
        // especially if the IP address suddenly changed. If there was a sudden
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          res.status(401).send({error: 'Unauthorized access. Please login again!'});
        } else {
          // Access is valid. Try to return data.
          getData(claims).then(data => {
            res.end(JSON.stringify(data);
          }, error => {
            res.status(500).send({ error: 'Server error!' })
          });
        }
      });
    });
    
    from firebase_admin import auth, initialize_app
    import flask
    
    initialize_app()
    flask_app = flask.Flask(__name__)
    
    @flask_app.post()
    def get_restricted_data(req: flask.Request):
        # Get the ID token passed.
        id_token = req.json().get("idToken")
    
        # Verify the ID token, check if revoked, and decode its payload.
        try:
            claims = auth.verify_id_token(id_token, check_revoked=True)
        except:
            return flask.Response(status=500)
    
        # Get request IP address.
        request_ip = req.remote_addr
    
        # Get sign-in IP address.
        signin_ip = claims["signInIpAddress"]
    
        # Check if the request IP address origin is suspicious relative to
        # the session IP addresses. The current request timestamp and the
        # auth_time of the ID token can provide additional signals of abuse,
        # especially if the IP address suddenly changed. If there was a sudden
        # geographical change in a short period of time, then it will give
        # stronger signals of possible abuse.
        if is_suspicious_change(signin_ip, request_ip):
            # Suspicious IP address change. Require re-authentication.
            # You can also revoke all user sessions by calling:
            #   auth.revoke_refresh_tokens(claims["sub"])
            return flask.Response(status=401,
                                  response="Unauthorized access. Sign in again!")
        else:
            # Access is valid. Try to return data.
            return data_from_claims(claims)
    

过滤用户照片

以下示例展示了如何清理用户的个人资料照片:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoUrl: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});
@identity_fn.before_user_created()
def sanitizeprofilephoto(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if event.data.photo_url is not None:
        score = analyze_photo_with_ml(event.data.photo_url)
        if score > THRESHOLD:
            return identity_fn.BeforeCreateResponse(photo_url=PLACEHOLDER_URL)

如需详细了解如何检测和清理图片,请参阅 Cloud Vision 文档。

访问用户的身份提供方的 OAuth 凭据

以下示例展示了如何为使用 Google 账号登录的用户获取刷新令牌,并使用该令牌调用 Google Calendar API。系统会存储刷新令牌以供离线访问时使用。

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (event.credential &&
      event.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        event.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // event.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: event.credential.accessToken,
            refresh_token: event.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});
@identity_fn.before_user_created()
def savegoogletoken(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    """During sign-up, save the Google OAuth2 access token and queue up a task
    to schedule an onboarding session on the user's Google Calendar.

    You will only get an access token if you enabled it in your project's blocking
    functions settings in the Firebase console:

    https://console.firebase.google.com/project/_/authentication/settings
    """
    if event.credential is not None and event.credential.provider_id == "google.com":
        print(f"Signed in with {event.credential.provider_id}. Saving access token.")

        firestore_client: google.cloud.firestore.Client = firestore.client()
        doc_ref = firestore_client.collection("user_info").document(event.data.uid)
        doc_ref.set({"calendar_access_token": event.credential.access_token}, merge=True)

        tasks_client = google.cloud.tasks_v2.CloudTasksClient()
        task_queue = tasks_client.queue_path(params.PROJECT_ID.value,
                                             options.SupportedRegion.US_CENTRAL1,
                                             "scheduleonboarding")
        target_uri = get_function_url("scheduleonboarding")
        calendar_task = google.cloud.tasks_v2.Task(http_request={
            "http_method": google.cloud.tasks_v2.HttpMethod.POST,
            "url": target_uri,
            "headers": {
                "Content-type": "application/json"
            },
            "body": json.dumps({
                "data": {
                    "uid": event.data.uid
                }
            }).encode()
        },
                                                   schedule_time=datetime.now() +
                                                   timedelta(minutes=1))
        tasks_client.create_task(parent=task_queue, task=calendar_task)

替换用户操作的 reCAPTCHA Enterprise 判定结果

以下示例展示了如何替换受支持的用户体验流程的 reCAPTCHA Enterprise 判定结果。

如需详细了解如何将 reCAPTCHA Enterprise 与 Firebase Authentication 集成,请参阅启用 reCAPTCHA Enterprise

屏蔽函数可用于根据自定义因素允许或屏蔽流程,从而替换 reCAPTCHA Enterprise 提供的结果。

Node.js
const { beforeSmsSent } = require("firebase-functions/v2/identity");
exports.beforesmssentv2 = beforeSmsSent((event) => {
 if (
   event.smsType === "SIGN_IN_OR_SIGN_UP" &&
   event.additionalUserInfo.phoneNumber.includes('+91')
 ) {
   return {
     recaptchaActionOverride: "ALLOW",
   };
 }

 // Allow users to sign in with recaptcha score greater than 0.5
 if (event.additionalUserInfo.recaptchaScore > 0.5) {
   return {
     recaptchaActionOverride: 'ALLOW',
   };
 }

 // Block all others.
 return  {
   recaptchaActionOverride: 'BLOCK',
 }
});