web-dev-qa-db-fra.com

Firestore Cloud: Application de noms d'utilisateur uniques

Le problème

J'ai vu cette question plusieurs fois (également dans le contexte de la base de données en temps réel Firebase), mais je n'y ai pas trouvé de réponse convaincante. L'énoncé du problème est assez simple:

Comment les utilisateurs (authentifiés) peuvent-ils choisir un nom d'utilisateur qui n'a pas encore été utilisé?

Tout d'abord, le why: après l'authentification d'un utilisateur, il dispose d'un ID utilisateur unique. Cependant, de nombreuses applications Web permettent à l'utilisateur de choisir un "nom d'affichage" (comment l'utilisateur souhaite apparaître sur le site Web), afin de protéger les données personnelles de l'utilisateur (comme un nom réel).

La collection des utilisateurs

Avec une structure de données comme celle-ci, il est possible de stocker un nom d'utilisateur avec d'autres données pour chaque utilisateur:

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

Cependant, rien n'empêche un autre utilisateur (avec un {uid} différent) de stocker la même name dans leur enregistrement. Autant que je sache, aucune "règle de sécurité" ne nous permet de vérifier si la name a déjà été faite par un autre utilisateur.

Remarque: une vérification côté client est possible, mais non sécurisée, car un client malveillant pourrait l'omettre.

La cartographie inverse

Les solutions populaires créent une collection avec une cartographie inverse:

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

Avec ce mappage inverse, il est possible d'écrire une règle de sécurité pour imposer qu'un nom d'utilisateur n'est pas déjà pris:

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

et pour forcer un utilisateur à créer d'abord un document de noms d'utilisateurs:

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

Je crois que la solution est à mi-chemin. Cependant, il reste encore quelques problèmes non résolus.

Autres questions/Questions

Cette implémentation est déjà compliquée mais elle ne résout même pas le problème des utilisateurs qui veulent changer leur nom d'utilisateur (nécessite la suppression d'un enregistrement ou des règles de mise à jour, etc.)

Un autre problème est que rien n'empêche un utilisateur d'ajouter plusieurs enregistrements dans la collection usernames, en arrachant efficacement tous les bons noms d'utilisateurs pour saboter le système.

Alors aux questions:

  • Existe-t-il une solution plus simple pour imposer des noms d'utilisateur uniques?
  • Comment éviter le spamming de la collection usernames?
  • Comment les contrôles de nom d'utilisateur peuvent-ils être rendus insensibles à la casse?

J'ai également essayé d'appliquer la règle users, avec une autre règle exists() pour la collection/usernames, puis de valider une opération d'écriture par lots. Cependant, cela ne semble pas fonctionner (erreur "manquant ou insuffisante").

Autre remarque: j'ai vu des solutions avec des contrôles côté client. MAIS CE SONT DANGEREUX. Tout client malveillant peut modifier le code et omettre les vérifications.

16
crazypeter

@asciimike sur Twitter est un développeur de règles de sécurité firebase ..__ Il ajoute qu’il n’existe actuellement aucun moyen d’appliquer l’unicité sur une clé d’un document. https://Twitter.com/asciimike/status/93703232291511025664

firestore étant basé sur Google Cloud datastore, il hérite de ce problème. Cette demande est formulée de longue date depuis 2008 . https://issuetracker.google.com/issues/35875869#c14

Cependant, vous pouvez atteindre votre objectif en utilisant firebase functions et quelques security rules stricts.

Vous pouvez voir toute la solution proposée sur support . https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952

16
jqualls

Crée une autre solution assez simple pour moi. 

J'ai une collection usernames pour stocker des valeurs uniques. username est disponible si le document n'existe pas, il est donc facile de le vérifier en frontal.

De plus, j'ai ajouté le motif ^([a-z0-9_.]){5,30}$ pour valider une valeur de clé.

Tout vérifier avec les règles 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);
}

Les règles Firestore n'autoriseront pas la mise à jour du document de l'utilisateur si le nom d'utilisateur existe déjà ou si sa valeur est invalide. 

Ainsi, les fonctions de cloud ne seront traitées que dans le cas où le nom d'utilisateur a une valeur valide et n'existe pas encore. Ainsi, votre serveur aura beaucoup moins de travail.

Tout ce dont vous avez besoin avec les fonctions cloud consiste à mettre à jour la collection 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;
  });
2
Bohdan Didukh

Créez une série de fonctions de cloud déclenchées chaque fois qu'un document est ajouté, mis à jour ou supprimé dans la table users. Les fonctions de cloud conservent une table de recherche distincte nommée usernames, avec les identifiants de document définis pour les noms d'utilisateur. Votre application frontale peut ensuite interroger la collection de noms d'utilisateurs pour savoir si un nom d'utilisateur est disponible.

Voici le code TypeScript pour les fonctions cloud:

/* 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();
  });
1
Derrick Miller

Je stocke la usernames dans la même collection où chaque nom d'utilisateur occupe un ID document unique. Ainsi, le nom d'utilisateur qui existe déjà ne sera pas créé dans la base de données. 

0
Artik