firebase google-cloud-firestore

firebase - Cloud Firestore: aplicación de nombres de usuario únicos



google-cloud-firestore (4)

El problema

He visto esta pregunta varias veces (también en el contexto de Firebase Real-Time Database), pero no he visto una respuesta convincente. La declaración del problema es bastante simple:

¿Cómo pueden los usuarios (autenticados) elegir un nombre de usuario que aún no se haya tomado?

En primer lugar, el por qué : después de que un usuario se autentica, tiene una identificación de usuario única. Sin embargo, muchas aplicaciones web permiten al usuario elegir un "nombre para mostrar" (cómo desea que el usuario aparezca en el sitio web), para proteger los datos personales de los usuarios (como el nombre real).

La colección de usuarios

Dada una estructura de datos como la siguiente, es posible almacenar un nombre de usuario junto con otros datos para cada usuario:

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

Sin embargo, nada impide que otro usuario (con un {uid} diferente) almacene el mismo name en su registro. Hasta donde sé, no existe una "regla de seguridad" que nos permita verificar si el name ya ha sido escrito por otro usuario.

Nota: es posible una verificación del lado del cliente, pero no es seguro ya que un cliente malintencionado podría omitir la verificación.

El mapeo inverso

Las soluciones populares están creando una colección con un mapeo inverso:

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

Dada esta asignación inversa, es posible escribir una regla de seguridad para exigir que un nombre de usuario aún no se haya tomado:

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

y para obligar a un usuario a crear primero un documento de nombres de usuario:

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

Creo que la solución está a medio camino. Sin embargo, todavía hay algunos problemas sin resolver.

Cuestiones / preguntas restantes

Esta implementación ya es bastante complicada, pero ni siquiera resuelve el problema de los usuarios que desean cambiar su nombre de usuario (requiere la eliminación de registros o reglas de actualización, etc.)

Otro problema es que nada impide que un usuario agregue múltiples registros en la colección de usernames , arrebatando efectivamente todos los buenos nombres de usuario para sabotear el sistema.

Entonces a las preguntas:

  • ¿Existe una solución más simple para imponer nombres de usuario únicos?
  • ¿Cómo se puede evitar el spam de la colección de usernames ?
  • ¿Cómo se pueden hacer las verificaciones de nombre de usuario sin distinción entre mayúsculas y minúsculas?

Intenté también imponer la existencia de los users , con otra regla exists() para la colección / usernames y luego cometí una operación de escritura por lotes, sin embargo, esto no parece funcionar (error " Permisos faltantes o insuficientes ").

Otra nota: he visto soluciones con controles del lado del cliente. PERO ESTOS SON INSEGUROS . Cualquier cliente malintencionado puede modificar el código y omitir verificaciones.


Creé otra solución bastante simple para mí.

Tengo una colección de usernames para almacenar valores únicos. username está disponible si el documento no existe, por lo que es fácil verificarlo en el front-end.

Además, agregué el patrón ^([a-z0-9_.]){5,30}$ para validar un valor clave.

Verificando todo con las reglas de 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); }

Las reglas de Firestore no permitirán actualizar el documento del usuario en caso de que el nombre de usuario ya exista o tenga un valor no válido.

Entonces, Cloud Functions se manejará solo en caso de que el nombre de usuario tenga un valor válido y aún no exista. Por lo tanto, su servidor tendrá mucho menos trabajo.

Todo lo que necesita con las funciones en la nube es actualizar la colección de 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; });


Cree una serie de funciones en la nube que se activan cada vez que se agrega, actualiza o elimina un documento en la tabla de users . Las funciones de la nube mantendrán una tabla de búsqueda separada llamada usernames , con identificadores de documento establecidos para los nombres de usuario. Su aplicación front-end puede consultar la colección de nombres de usuario para ver si hay un nombre de usuario disponible.

Aquí está el código TypeScript para las funciones de la nube:

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


Guardo los usernames de usernames en la misma colección donde cada nombre de usuario ocupa una ID de document única. De esa forma, el nombre de usuario que ya existe no se creará en la base de datos.


@asciimike en twitter es un desarrollador de reglas de seguridad de Firebase. Él dice que actualmente no hay forma de imponer la unicidad en una clave en un documento. https://twitter.com/asciimike/status/937032291511025664

Dado que firestore se basa en el datastore Google Cloud, hereda este problema. Ha sido una solicitud de larga data desde 2008. https://issuetracker.google.com/issues/35875869#c14

Sin embargo, puede lograr su objetivo utilizando firebase functions y algunas estrictas security rules .

Puede ver toda mi solución propuesta en medio. https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952