首页 > 解决方案 > 使用 Firebase 云功能验证 Firebase 应用中是否存在电话号码

问题描述

我是 firebase(及其所有功能)空间的新手。我已经阅读了文档,并且已经能够正确使用 web sdk。我创建了一个文件,其中编写了我所有当前的 firebase 代码,如下面的 firebaseApi.js 所示。此外,下面是我如何使用registration.js 下的函数的示例(如果我做错了,请纠正),该示例有效。我试图实施

 admin.auth().getUserByPhoneNumber(phoneNumber),

我想用它来检查应用程序中是否已经存在当前输入的电话号码。但我已阅读 Admin SDK 不能在客户端环境中使用,只能在 Firebase 应用程序的开发人员拥有或管理的特权服务器环境中使用。我有点迷失了如何解决这个问题。是否可以像我使用 firebaseApi 一样将 firebase 云功能连接到客户端?我已经清理了代码并只保留了相关部分

firebaseApi.js

        import firebase from 'firebase/app';
        import 'firebase/firestore';
        import 'firebase/auth';
        import 'firebase/database';
        import 'firebase/storage';

        const config = {config};

        firebase.initializeApp(config);

        class Firebase {
          register = ({ fullname, email, phone }) => {
            const user = Firebase.auth.currentUser.uid;
            const firestoreRef = Firebase.firestore.collection('Users').doc(user);

            const settings = {
              fullname,
              email,
              phone,
            };

            firestoreRef
              .set(settings);
          };

          static init() {
            Firebase.auth = firebase.auth();
            Firebase.firestore = firebase.firestore();
            Firebase.database = firebase.database();
            Firebase.storage = firebase.storage();
            Firebase.email = firebase.auth.EmailAuthProvider;
            Firebase.google = firebase.auth.GoogleAuthProvider;
            Firebase.phoneVerify = new firebase.auth.PhoneAuthProvider();
            Firebase.phone = firebase.auth.PhoneAuthProvider;
          }
        }

        Firebase.shared = new Firebase();
        export default Firebase;

注册.js

          import Firebase from './firebaseApi';

          onCompleteReg() {
            const { fullname, email, email } = this.state;

            const settings = {
              fullname,
              email,
              email
            };

            Firebase.shared
              .registerSettings(settings)
              .then(() => {
                console.log('Successful');
              }).catch((e) => {
                console.log(e);
              })
          }

标签: node.jsreactjsfirebasefirebase-authenticationgoogle-cloud-functions

解决方案


作为隐私和最佳实践的问题,除非当前用户是管理员,否则我不会公开检查任何给定电话号码是否被任何个人使用和/或是否与您的应用程序相关联的能力。

包裹在云函数中

由于 Admin SDK 只能在安全环境中使用,因此您只能通过某些 API 公开其功能。在这种情况下,自动处理用户身份验证和 CORS 是有益的,因此我将使用Callable Function。基于这种 API 的敏感性,还建议对它的访问进行速率限制,这可以使用firebase-functions-rate-limiter包轻松实现。在下面的代码中,我们将 API 调用限制为每个用户 2 次使用,所有用户每 15 秒使用 10 次,以防止滥用。

import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { FirebaseFunctionsRateLimiter } from 'firebase-functions-rate-limiter';

admin.initializeApp();

const realtimeDb = admin.database();

const perUserLimiter = FirebaseFunctionsRateLimiter.withRealtimeDbBackend(
    {
        name: 'rate-limit-phone-check',
        maxCalls: 2,
        periodSeconds: 15,
    },
    realtimeDb
);
const globalLimiter = FirebaseFunctionsRateLimiter.withRealtimeDbBackend(
    {
        name: 'rate-limit-phone-check',
        maxCalls: 10,
        periodSeconds: 15,
    },
    realtimeDb
);

exports.phoneNumber = functions.https.onCall(async (data, context) => {
  // assert required params
  if (!data.phoneNumber) {
    throw new functions.https.HttpsError(
        'invalid-argument',
        'Value for "phoneNumber" is required.'
      );
  } else if (!context.auth || !context.auth.uid) {
    throw new functions.https.HttpsError(
        'failed-precondition',
        'The function must be called while authenticated.'
      );
  }

  // rate limiter
  const [userLimitExceeded, globalLimitExceeded] = await Promise.all(
      perUserLimiter.isQuotaExceededOrRecordUsage('u_' + context.auth.uid),
      globalLimiter.isQuotaExceededOrRecordUsage('global'));
  if (userLimitExceeded || globalLimitExceeded) {
    throw new functions.https.HttpsError(
        'resource-exhausted',
        'Call quota exceeded. Try again later',
      );
  }

  let userRecord = await admin.auth.getUserByPhoneNumber(phoneNumber);
  return userRecord.uid;
}

要调用检查,您将在客户端上使用以下代码:

let checkPhoneNumber = firebase.functions().httpsCallable('phoneNumber');

checkPhoneNumber({phoneNumber: "61123456789"})
  .then(function (result) {
    let userId = result.data;
    // do something with userId
  })
  .catch(function (error) {
    console.error('Failed to check phone number: ', error)
  });

登录尝试

与其让用户查明电话号码是否存在或明确存在于您的服务中,不如遵循电话号码身份验证流程并允许他们证明他们拥有给定的电话号码。由于用户无法一次性验证多个号码,因此这是最安全的方法。

Firebase Phone Auth Reference中,以下代码用于验证电话号码:

// 'recaptcha-container' is the ID of an element in the DOM.
var applicationVerifier = new firebase.auth.RecaptchaVerifier(
    'recaptcha-container');
var provider = new firebase.auth.PhoneAuthProvider();
provider.verifyPhoneNumber('+16505550101', applicationVerifier)
    .then(function(verificationId) {
      var verificationCode = window.prompt('Please enter the verification ' +
          'code that was sent to your mobile device.');
      return firebase.auth.PhoneAuthProvider.credential(verificationId,
          verificationCode);
    })
    .then(function(phoneCredential) {
      return firebase.auth().signInWithCredential(phoneCredential);
    });

特权电话搜索

如果您希望具有适当特权的用户(无论他们具有管理员还是管理角色)能够通过电话号码查询用户,您可以使用以下脚手架。在这些代码示例中,我将访问权限限制为isAdmin对其身份验证令牌拥有所有权的人。

数据库结构:(有关更多信息,请参阅此答案)

"phoneNumbers": {
  "c011234567890": { // with CC for US
    "userId1": true
  },
  "c611234567890": { // with CC for AU
    "userId3": true
  },
  ...
}

数据库规则:

{
  "rules": {
    ...,
    "phoneNumbers": {
      "$phoneNumber": {
        "$userId": {
          ".write": "auth.uid === $userId && (!newData.exists() || root.child('users').child(auth.uid).child('phoneNumber').val() == ($phoneNumber).replace('c', ''))" // only this user can edit their own record and only if it is their phone number or they are deleting this record
        }
      },
      ".read": "auth != null && auth.token.isAdmin == true", // admins may read/write everything under /phoneNumbers
      ".write": "auth != null && auth.token.isAdmin == true"
    }
  }
}

辅助功能:

function doesPhoneNumberExist(phoneNumber) {
  return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).once('value')
    .then((snapshot) => snapshot.exists());
}
// usage: let exists = await doesPhoneNumberExist("611234567890")

function getUsersByPhoneNumber(phoneNumber) {
  return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).once('value')
    .then((snapshot) => snapshot.exists() ? Object.keys(snapshot.val()) : []);
}
// usage: let usersArray = await getUsersByPhoneNumber("611234567890") - normally only one user

function searchPhoneNumbersThatStartWith(str) {
  if (!str || str.length < 5) return Promise.reject(new Error('Search string is too short'));
  return firebase.database.ref("phoneNumbers").startAt("c" + str).endAt("c" + str + "\uf8ff").once('value')
    .then((snapshot) => {
      let phoneNumbers = [];
      snapshot.forEach((phoneEntrySnapshot) => phoneNumbers.push(phoneEntrySnapshot.key));
      return phoneNumbers;
    });
}
// usage: let matches = await searchPhoneNumbersThatStartWith("61455")

// best handled by Cloud Function not client
function linkPhoneNumberWithUser(phoneNumber, userId) {
  return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).child(userId).set(true);
}
// usage: linkPhoneNumberWithUser("611234567890", firebase.auth().currentUser.uid)

// best handled by Cloud Function not client
function unlinkPhoneNumberWithUser(phoneNumber, userId) {
  return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).child(userId).remove();
}
// usage: unlinkPhoneNumberWithUser("611234567890", firebase.auth().currentUser.uid)

推荐阅读