【问题标题】:Cloud Firestore: Enforcing Unique User NamesCloud Firestore:强制使用唯一用户名
【发布时间】:2018-05-04 11:37:35
【问题描述】:

问题

我已经多次看到这个问题(也在 Firebase 实时数据库的背景下),但我还没有看到令人信服的答案。问题陈述相当简单:

(经过身份验证的)用户如何选择尚未使用的用户名?

首先,为什么:用户认证后,他们有一个唯一的用户 ID。然而,许多网络应用程序让用户选择“显示名称”(用户希望如何出现在网站上),以保护用户的个人数据(如真实姓名)。

用户集合

给定如下数据结构,可以为每个用户存储用户名和其他数据:

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

但是,没有什么能阻止其他用户(使用不同的{uid})在他们的记录中存储相同的name。据我所知,没有“安全规则”可以让我们检查name 是否已被其他用户使用。

注意:客户端检查是可能的,但不安全,因为恶意客户端可能会忽略检查。

反向映射

流行的解决方案是使用反向映射创建集合:

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

鉴于这种反向映射,可以编写一个安全规则来强制用户名尚未被使用:

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;
}

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

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() 规则,然后提交批量写入操作,但是,这似乎不起作用(“缺失或不足权限”错误)。

另一个注意事项:我已经看到了带有客户端检查的解决方案。 但这些都不安全。任何恶意客户端都可以修改代码,省略检查。

【问题讨论】:

  • 有人在 Firestore twittert 上工作,我相信他建议没有必要创建反向查找:twitter.com/abeisgreat/status/920730970751254528
  • 你有没有想出一个可靠的解决方案?
  • 我基本上是在使用上面的解决方案,我实现了支持更新用户名的规则,但是恶意垃圾邮件和不区分大小写的匹配仍然是一个悬而未决的问题。
  • 如何将客户端检查移至函数?让用户向函数发送“更改用户名请求”,并在那里进行检查。如果成功,则从应该是只读的函数中设置用户的用户名。
  • 我很确定这是函数的主要用例之一。从设备上卸载密集的数据处理(例如缩略图生成),在安全的环境中执行敏感操作(例如强制使用唯一用户名)并发送推送通知/电子邮件(ofc。还有更多)。

标签: firebase google-cloud-firestore


【解决方案1】:

@asciimike on twitter 是 firebase 安全规则开发人员。 他说,目前没有办法强制文档上的密钥具有唯一性。 https://twitter.com/asciimike/status/937032291511025664

由于firestore 基于谷歌云datastore,它继承了这个问题。自 2008 年以来,这是一个长期存在的要求。 https://issuetracker.google.com/issues/35875869#c14

但是,您可以通过使用firebase functions 和一些严格的security rules 来实现您的目标。

您可以在 medium 上查看我提出的整个解决方案。 https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952

【讨论】:

  • 这对于 web 应用来说是一个很棒的解决方法,但是如何通过新的 firestore 对本机应用强制执行此操作呢?
  • 这方面有什么更新吗?还没有内置 Firestore 解决方案?
  • @Mr.Drew 你找到解决方案了吗?
  • @Ahsan,不是真的,但是对于不同的解决方法有一些策略。想到的是创建一个单独的当前用户名集合,以便在您每次要添加或更改用户名时进行检查。这需要更多客户端代码或 firebase 函数来检查用户名集合。
  • 如果我错了,请纠正我,但在您的云功能第 51 行: if (unameDoc.exists && unameDoc.data.uid !== req.user.uid) 我相信您的意思是 ===
【解决方案2】:

为我创建了另一个非常简单的解决方案。

我有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 规则将不允许更新用户的文档。

因此,Cloud Functions 将仅在用户名具有有效值且尚不存在的情况下进行处理。因此,您的服务器的工作量将大大减少。

使用云功能所需的一切就是更新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;
  });

【讨论】:

  • 在花了几个小时弄清楚如何让它工作之后(我一直得到无效的身份验证,因为我没有注意你的正则表达式规则,{ userID } 这行应该是一个键值对)我终于明白了,尽管我必须添加行 admin.initializeApp(functions.config().firebase); 如上所述 here in the docs for node.js
  • @Mr.Drew SDK也可以不带参数初始化。在这种情况下,SDK 使用 Google 应用程序默认凭据并从 FIREBASE_CONFIG 环境变量中读取选项。 You can read more information here
  • 啊,我现在明白了,谢谢。但我想知道为什么我不必为实时数据库函数文件执行此操作,但我为这个 firestore 文件执行了此操作?万一任何人开始偶然发现此检查 here 以获取有关服务器到服务器通信中 GOOGLE_APPLICATION_CREDENTIALS 的更多信息。由于它仍处于测试阶段,我正在尝试熟悉它,而不是在更大范围的项目中实施它。因此,第一个文档链接中的默认说明对我有用。
  • 如果用户还没有用户名而我想更改 users/{ userID } 中的其他字段怎么办
  • @EmreAkkoc, - 如果用户名与以前相同或有效,则允许更新其他字段的安全规则。 - !exists(/databases/{database}/documents/usernames/$(username)) 仅当用户名存在时返回 false。
【解决方案3】:

创建一系列在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();
  });

【讨论】:

  • 您使用哪些安全规则来防止来自客户端的篡改?
  • 我会发布整个 firestore.rules 文件,但它太长了。以下是与用户名集合相关的行: // USERNAMES match /usernames/{username} { allow get: if true; // 只能查询单个用户名以查看它们是否可用允许列表: if false; // 无法获取用户名列表 allow create: if false; // 不能修改 - 由云函数处理 allow update: if false; // 不能修改 - 由云函数处理 allow delete: if false; // 不能修改 - 由云函数处理 }
  • 谢谢@Derrick,您对 /users/{userId} 本身有什么规定?
  • @MiguelStevens // 用户匹配 /users/{userId} { 允许获取:如果为真; // 需要允许查看用户配置文件允许列表:如果为真; // 这个可以收紧吗?允许创建:如果 request.auth.uid != null && request.resource.data.username == '' && request.resource.data.isAdmin == 0; // 不允许用户升级自己的权限 allow update: if request.auth.uid == userId && request.resource.data.isAdmin == 0;允许删除:如果为假; }
【解决方案4】:

这对我很有效,因为用户名必须是唯一的。我可以添加和编辑用户名而不重复。

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

创建用户集合:

/users(集合)

/{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;
    }
    
  }

【讨论】:

  • 要创建一个用户,他的记录必须首先出现在 /usernames 表中,对吗?
【解决方案5】:

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

【讨论】:

  • 这不会丧失用户名转移的权利吗?至少您需要一种更复杂的检查方法来检查所有唯一文档 ID 并检查它们的字段。
【解决方案6】:

一种可能的解决方案是将所有用户名存储在单个文档的 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();
}

如果您想从用户名 -> uid 进行映射(用于验证规则集的其他部分),这也可以在单个文档中进行。你可以只取文档的keyset,做和上面一样的set操作。

【讨论】:

    【解决方案7】:

    此答案解决了您对在用户名集合中添加多条记录的第二个顾虑。我不确定这是否是最好的方法,但我相信防止给定用户创建多个用户名文档的可能方法是编写一个onCreate 云函数,该函数在新用户名时检查用户是否有现有的用户名文档创建文档。如果用户这样做了,那么云功能可以删除该文档,以防止任何恶意用户名停放。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-10
      • 2018-04-28
      • 1970-01-01
      • 1970-01-01
      • 2020-12-11
      • 2018-04-23
      • 2018-11-11
      相关资源
      最近更新 更多