Cloud Firestore: использование уникальных имен пользователей

Проблема

Я видел этот вопрос несколько раз (также в контексте базы данных реального времени Firebase), но я не видел убедительного ответа на него. Вывод задачи довольно прост:

Как пользователи, прошедшие проверку подлинности, выбирают имя пользователя, которое еще не было принято?

Прежде всего, почему: после аутентификации пользователя у них есть уникальный идентификатор пользователя. Однако многие веб-приложения позволяют пользователю выбрать "отображаемое имя" (как пользователь хочет показать на веб-сайте), чтобы защитить персональные данные пользователей (например, настоящее имя).

Коллекция пользователей

Для структуры данных, подобной следующей, можно сохранить имя пользователя вместе с другими данными для каждого пользователя:

/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, с другим правилом exists() для коллекции /usernames и затем выполнить операцию пакетной записи, однако это, похоже, не работает (ошибка "Отсутствующие или недостаточные разрешения" )).

Еще одно замечание: я видел решения с клиентскими проверками. НО ЭТИ НЕСООТВЕТСТВУЮТ. Любой вредоносный клиент может изменить код и пропустить проверки.

Ответ 1

@asciimike в twitter - разработчик правил безопасности firebase. Он говорит, что в настоящее время нет способа обеспечить уникальность ключа в документе. https://twitter.com/asciimike/status/937032291511025664

Так как firestore основан на Google Cloud datastore, он наследует эту проблему. Это была давняя просьба с 2008 года. https://issuetracker.google.com/issues/35875869#c14

Однако вы можете достичь своей цели, используя firebase functions и некоторые строгие security rules.

Вы можете просмотреть все предлагаемое решение на носителе. https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952

Ответ 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 не позволяют обновлять пользовательский документ в случае, если имя пользователя уже существует или имеет недопустимое значение.

Таким образом, облачные функции будут обрабатываться только в том случае, если имя пользователя имеет допустимое значение и еще не существует. Таким образом, ваш сервер будет иметь гораздо меньше работы.

Все, что вам нужно с облачными функциями, это обновить коллекцию 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;
  });

Ответ 3

Создайте серию облачных функций, которые запускаются всякий раз, когда документ добавляется, обновляется или удаляется в таблице users. Облачные функции будут поддерживать отдельную таблицу поиска с именем usernames, с идентификаторами документов, установленными на usernames. Затем ваше интерфейсное приложение может запросить коллекцию имен пользователей, чтобы узнать, доступно ли имя пользователя.

Вот код 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();
  });

Ответ 4

Я храню usernames в той же коллекции, где каждое имя пользователя занимает уникальный идентификатор document. Таким образом, имя пользователя, которое уже существует, не будет создано в базе данных.