firebase 云火:强制唯一用户名

6gpjuf90  于 2022-11-17  发布在  其他
关注(0)|答案(8)|浏览(161)

"问题"
这个问题我已经看过好几次了(也是在Firebase实时数据库的上下文中),但是我还没有看到一个令人信服的答案。

(已验证的)用户如何选择尚未使用的用户名?

首先,* 为什么 *:用户通过身份验证后,他们将拥有一个唯一的用户ID。然而,许多Web应用程序允许用户选择“显示名称”(用户希望在网站上的显示方式),以保护用户的个人数据(如真实的姓名)。

用户集合

给定如下所示的数据结构,可以将用户名与每个用户的其他数据沿着存储:

/users  (collection)
    /{uid}  (document)
        - name: "<the username>"
        - foo: "<other data>"

但是,没有什么可以阻止另一个用户(使用不同的{uid})在他们的记录中存储相同的name。据我所知,没有“安全规则”允许我们检查name是否已经被另一个用户使用。
注意:客户端检查是可能的,但不安全,因为恶意客户端可能忽略检查。

反向Map

常用的解决方案是使用反向Map创建集合:

/usernames  (collection)
    /{name}  (document)
       - uid: "<the auth {uid} field>"

给定此反向Map,可以编写一个安全规则来强制用户名尚未被使用:

match /users/{userId} {
  allow read: if true;
  allow create, update: if
      request.auth.uid == userId &&
      request.resource.data.name is string &&
      request.resource.data.name.size() >= 3 &&
      get(/PATH/usernames/$(request.resource.data.name)).data.uid == userId;
}

并强制用户首先创建usernames文档:

match /usernames/{name} {
  allow read: if true;
  allow create: if
      request.resource.data.size() == 1 &&
      request.resource.data.uid is string &&
      request.resource.data.uid == request.auth.uid;
}

我相信问题已解决了一半,但仍有一些问题尚未解决。

剩馀问题

这种实现已经相当复杂,但它甚至没有解决用户想要 * 更改其用户名 * 的问题(需要记录删除或更新规则等)。
另一个问题是,没有什么可以阻止用户在usernames集合中添加多个记录,从而有效地夺取所有好的用户名来破坏系统。
于是来了个疑问:

  • 是否有更简单的解决方案来强制使用唯一的用户名?
  • 如何防止垃圾邮件发送usernames集合?
  • 如何使用户名检查不区分大小写?

我还尝试强制users的存在,使用/usernames集合的另一个exists()规则,然后提交批写操作,但是,这似乎不起作用(“* 缺少或权限不足 ”错误)。
另一个注意事项:我看到过带有客户端检查的解决方案。
但这些都是不安全的 *。任何恶意客户端都可以修改代码,并省略检查。

0vvn1miw

0vvn1miw1#

twitter上的@asciimike是firebase安全规则的开发者,他说目前还没有办法强制文档密钥的唯一性。
由于firestore基于Google Cloud datastore,因此它继承了这个问题。自2008年以来,这是一个长期存在的请求。https://issuetracker.google.com/issues/35875869#c14
但是,您可以通过使用firebase functions和一些严格的security rules来实现您的目标。
您可以在medium上查看我建议的整个解决方案。https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952

x6yk4ghg

x6yk4ghg2#

为我创造了另一个非常简单的解决方案。
我有usernames集合来存储唯一值。如果文档不存在,username可用,所以很容易在前端检查。
此外,我还添加了模式^([a-z0-9_.]){5,30}$来验证键值。
使用Firestore规则检查所有内容:

function isValidUserName(username){
  return username.matches('^([a-z0-9_.]){5,30}$');
}

function isUserNameAvailable(username){
  return isValidUserName(username) && !exists(/databases/$(database)/documents/usernames/$(username));
}

match /users/{userID} {
  allow update: if request.auth.uid == userID 
      && (request.resource.data.username == resource.data.username
        || isUserNameAvailable(request.resource.data.username)
      );
}

match /usernames/{username} {
  allow get: if isValidUserName(username);
}

如果用户名已存在或具有无效值,Firestore规则将不允许更新用户的文档。
所以,云函数只会在用户名有一个有效值但还不存在的情况下进行处理。所以,你的服务器将有更少的工作。
您需要使用云函数来更新usernames集合:

const functions = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp(functions.config().firebase);

exports.onUserUpdate = functions.firestore
  .document("users/{userID}")
  .onUpdate((change, context) => {
    const { before, after } = change;
    const { userID } = context.params;

    const db = admin.firestore();

    if (before.get("username") !== after.get('username')) {
      const batch = db.batch()

      // delete the old username document from the `usernames` collection
      if (before.get('username')) {
        // new users may not have a username value
        batch.delete(db.collection('usernames')
          .doc(before.get('username')));
      }

      // add a new username document
      batch.set(db.collection('usernames')
        .doc(after.get('username')), { userID });

      return batch.commit();
    }
    return true;
  });
xe55xuns

xe55xuns3#

创建一系列云函数,每当在users表中添加、更新或删除文档时都会触发这些云函数。这些云函数将维护一个名为usernames的单独查找表,并将文档ID设置为用户名。然后,您的前端应用可以查询用户名集合,以查看是否有可用的用户名。
下面是云函数的TypeScript代码:

/* Whenever a user document is added, if it contains a username, add that
   to the usernames collection. */
export const userCreated = functions.firestore
  .document('users/{userId}')
  .onCreate((event) => {

    const data = event.data();
    const username = data.username.toLowerCase().trim();

    if (username !== '') {
      const db = admin.firestore();
      /* just create an empty doc. We don't need any data - just the presence 
         or absence of the document is all we need */
      return db.doc(`/usernames/${username}`).set({});
    } else {
      return true;
    }

  });

  /* Whenever a user document is deleted, if it contained a username, delete 
     that from the usernames collection. */
  export const userDeleted = functions.firestore
    .document('users/{userId}')
    .onDelete((event) => {

      const data = event.data();
      const username = data.username.toLowerCase().trim();

      if (username !== '') {
        const db = admin.firestore();
        return db.doc(`/usernames/${username}`).delete();
      }
      return true;
    });

/* Whenever a user document is modified, if the username changed, set and
   delete documents to change it in the usernames collection.  */
export const userUpdated = functions.firestore
  .document('users/{userId}')
  .onUpdate((event, context) => {

    const oldData = event.before.data();
    const newData = event.after.data();

    if ( oldData.username === newData.username ) {
      // if the username didn't change, we don't need to do anything
      return true;
    }

    const oldUsername = oldData.username.toLowerCase().trim();
    const newUsername = newData.username.toLowerCase().trim();

    const db = admin.firestore();
    const batch = db.batch();

    if ( oldUsername !== '' ) {
      const oldRef = db.collection("usernames").doc(oldUsername);
      batch.delete(oldRef);
    }

    if ( newUsername !== '' ) {
      const newRef = db.collection("usernames").doc(newUsername);
      batch.set(newRef,{});
    }

    return batch.commit();
  });
6ie5vjzr

6ie5vjzr4#

这对我来说很有效,用户名必须是唯一的。我能够添加和编辑用户名,没有重复。

**注意:***用户名必须始终为小写 *,这将消除因区分大小写而导致的重复。

创建用户集合:
/用户(收藏)

/{uid} (document)
      - name "the username"

创建用户名集合:
/使用者名称(集合)

/{name} (document)
       - uid "the auth {uid} field"

然后在firestore中使用以下规则:

match /databases/{database}/documents {
    
match /usernames/{name} {
  allow read,create: if request.auth != null;
  allow update: if 
        request.auth.uid == resource.data.uid;
}

match /users/{userId}{
    allow read: if true;
    allow create, update: if 
      request.auth.uid == userId && 
      request.resource.data.name is string && 
      request.resource.data.name.size() >=3 && 
      get(/databases/$(database)/documents/usernames/$(request.resource.data.name)).data.uid == userId;
    }
    
  }
63lcw9qa

63lcw9qa5#

我将usernames存储在同一个集合中,其中每个用户名都占用一个唯一的document ID。这样,数据库中就不会创建已经存在的用户名。

cl25kdpy

cl25kdpy6#

一种可能的解决方案是将所有用户名存储在单个文档的usernames字段中,然后使用规则中的集合仅允许向该文档添加用户名:

match /users/allUsernames {
  function validateNewUsername() {
    // Variables in functions are allowed.
    let existingUsernames = resource.data.usernames;
    let newUsernames = request.resource.data.usernames;
    let usernameToAdd = newUsernames[newUsernames.size() - 1];
    // Sets are a thing too.
    let noRemovals = existingUsernames.toSet().difference(newUsernames.toSet()).size() == 0;
    let usernameDoesntExistYet = !(usernameToAdd in existingUsernames.toSet());
    let exactlyOneAddition = newUsernames.size() == existingUsernames.size() + 1;
    return noRemovals && usernameDoesntExistYet && exactlyOneAddition;
  }
  allow update: if request.resource.data.keys().hasOnly(['usernames']) && validateNewUsername();
}

如果你想从username -〉uid进行Map(用于验证规则集的其他部分),这在一个文档中也是可能的。你可以只使用文档的键集,并执行与上面相同的设置操作。

sgtfey8w

sgtfey8w7#

这个 答案 解决 了 您 的 第 二 个 问题 , 即 在 usernames 集合 中 添加 多 个 记录 。 我 不 确定 这 是否 是 最 好 的 方法 , 但 我 相信 防止 给定 用户 创建 多 个 用户 名 文档 的 一 种 可能 方法 是 编写 一 个 onCreate 云 函数 , 该 函数 在 创建 新 的 用户 名 文档 时 检查 用户 是否 已有 用户 名 文档 。 如果 用户 已有 ,则 云 函数 可以 删除 该 文档 以 防止 任何 恶意 的 用户 名 驻留 。

7rfyedvj

7rfyedvj8#

将数据库中使用的最大整数用户id存储在另一个集合中。每次查询该集合以查找最大用户id。您甚至可以将其他最大id存储在该集合中。它可以类似于以下内容:

MaxIDCollection:
   maxStudentIDDocument={ maxID: 55 } //lets say the max user id in db is 55
   maxCourseIDDocument={ maxID: 77 }

请确保每次添加新学生或课程时都更新maxIDs。如果以后添加新学生,则通过查询此集合,您可以知道“如果最大值为55,则新学生的ID应为56”。

相关问题