Hay muchos casos de uso para las bases de datos NoSQL, uno que encuentro con frecuencia es la creación de un almacén de perfiles de usuario y sesiones. Este caso de uso se presta a una Base de datos NoSQL. A menudo, los perfiles deben ser flexibles y aceptar cambios en los datos. Aunque es posible en un RDBMS, requeriría más trabajo mantener los datos con penalizaciones de rendimiento.
Kirk Kirkconnell escribió un ejemplo de alto nivel para crear un almacén y una sesión de perfil de usuario con Couchbase: Almacén de perfiles de usuario: Modelado avanzado de datos. Vamos a ampliar estos conceptos e implementar un almacén de perfiles de usuario utilizando bases de datos Node.js y Servidor Couchbase.
Este tutorial ha sido actualizado el 4 de enero de 2021 por Eric Bishard para trabajar con Couchbase SDK de NodeJS 3!
Antes de escribir algo de código, vamos a averiguar lo que estamos tratando de lograr.
Cuando se trata de gestionar los datos de un usuario, necesitamos una forma de crear un almacén de perfiles de usuario y una sesión, y asociarles otros documentos. Definamos algunas reglas en torno a este concepto de almacén de perfiles de usuario:
- Almacene los datos de la cuenta, como el nombre de usuario y la contraseña, en un documento de perfil.
- Pasar datos sensibles del usuario con cada solicitud de acción del usuario.
- Utiliza una sesión que expira después de un tiempo determinado.
- Documentos de sesión almacenados con un límite de caducidad.
Podemos gestionar todo esto con los siguientes puntos finales de la API:
- POST /account - Crear un nuevo perfil de usuario con información de la cuenta
- POST /login - Validar la información de la cuenta
- GET /account - Obtener información de la cuenta
- POST /blog - Crear una nueva entrada de blog asociada a un usuario
- GET /blogs - Obtener todas las entradas de blog de un usuario concreto
Estos endpoints formarán parte de nuestro API backend utilizando el SDK Node.js de Couchbase Server (este artículo ha sido actualizado para utilizar la versión 3.1.x).
Creación de la API con Node y Express
Vamos a crear un directorio de proyecto para nuestra aplicación Node.js e instalar nuestras dependencias.
1 2 |
mkdir blog-api && cd blog-api && npm init -y npm instale couchbase express cuerpo-analizador uuid bcryptjs cors --guardar |
Esto crea un directorio de trabajo para nuestro proyecto e inicializa un nuevo proyecto Node. Nuestras dependencias incluyen SDK de Node.js para Couchbase y Express Framework y otras bibliotecas de utilidades como body-parser
para aceptar datos JSON a través de peticiones POST, uuid
para generar claves únicas y bcryptjs
hash de nuestras contraseñas para disuadir a los usuarios malintencionados.
Vamos a arrancar nuestra aplicación con un archivo servidor.js file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const couchbase = requiere(couchbase) const express = requiere(exprés) const uuid = requiere(uuid) const bodyParser = requiere('body-parser') const bcrypt = requiere(bcryptjs) const cors = requiere(cors) const aplicación = express() aplicación.utilice(cors()) aplicación.utilice(bodyParser.json()) aplicación.utilice(bodyParser.urlencoded({ ampliado: verdadero })) const grupo = nuevo couchbase.Grupo(couchbase://localhost, { nombre de usuario: Administrador, contraseña: contraseña }) const cubo = grupo.cubo(blog) const colección = cubo.defaultCollection() const servidor = aplicación.escuche(3000, () => consola.información(`Ejecutar en puerto ${servidor.dirección().puerto}...`)) |
El código anterior requiere nuestras dependencias e inicializa una aplicación Express que se ejecuta en el puerto 3000 contra Couchbase Server utilizando un bucket llamado blog
.
También necesitamos crear un índice en Couchbase Server porque usaremos el lenguaje de consulta N1QL para uno de nuestros endpoints. Si accedemos a nuestra consola web de Couchbase Server ejecutándose localmente en localhost:8091podemos hacer clic en el botón Consulta y ejecute esta sentencia en el Editor de Consultas:
1 |
CREAR ÍNDICE `blogbyuser` EN `por defecto`(tipo, pid); |
Dado que obtendremos todas las entradas de blog para un id de perfil concreto, obtendremos un mejor rendimiento utilizando este índice específico en lugar de un índice primario general. Los índices primarios no se recomiendan para código de nivel de producción.
Guardar un nuevo usuario en el almacén de perfiles
Sabemos que un perfil de usuario puede contener cualquier información que describa a un usuario. Información como dirección, teléfono, redes sociales, etc. Nunca es buena idea almacenar las credenciales de la cuenta en el mismo documento que la información básica de nuestro perfil. Necesitaremos un mínimo de dos documentos para cada usuario, echemos un vistazo a cómo se estructurarán esos documentos.
Nuestro documento de perfil tendrá una clave a la que haremos referencia en nuestros documentos relacionados. Esta clave es un UUID autogenerado: b181551f-071a-4539-96a5-8a3fe8717faf
.
Nuestro documento de perfil tendrá un valor JSON que incluye dos propiedades: correo electrónico
y un tipo
propiedad. En tipo
es un indicador importante que describe nuestro documento de forma similar a como una tabla organiza los registros en una base de datos relacional. Se trata de una convención estándar en una base de datos de documentos.
1 2 3 4 |
{ "tipo": "perfil", "email": "user1234@gmail.com" } |
El documento de cuenta asociado a nuestro perfil tendrá una clave igual al email de nuestro usuario:
user1234@gmail.com
y este documento tendrá un tipo
así como un pid
refiriéndose a la clave de nuestro documento de perfil junto con correo electrónico
y hash contraseña
.
1 2 3 4 5 6 |
{ "tipo": "cuenta", "pid": "b181551f-071a-4539-96a5-8a3fe8717faf", "email": "user1234@gmail.com", "contraseña": "$2a$10$tZ23pbQ1sCX4BknkDIN6NekNo1p/Xo.Vfsttm.USwWYbLAAspeWsC" } |
Genial, hemos establecido un modelo para cada documento y una estrategia para relacionar esos documentos sin restricciones de base de datos.
Un punto final para la creación de cuentas
Añada el siguiente código a nuestro servidor.js
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
aplicación.Correo electrónico:("/cuenta", async (solicitar, respuesta) => { si (!solicitar.cuerpo.correo electrónico) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un `email`" }) } si no si (!solicitar.cuerpo.contraseña) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere una `contraseña`" }) } const id = uuid.v4() const cuenta = { "tipo": "cuenta", "pid": id, "email": solicitar.cuerpo.correo electrónico, "contraseña": bcrypt.hashSync(solicitar.cuerpo.contraseña, 10) } const perfil = { "tipo": "perfil", "email": solicitar.cuerpo.correo electrónico } await colección.insertar(id, perfil) .entonces(async () => { await colección.insertar(solicitar.cuerpo.correo electrónico, cuenta) .entonces((resultado) => { resultado.pid = id devolver respuesta.enviar(resultado) }) .captura(async (e) => { await colección.eliminar(id) .entonces(() => { consola.error(`cuenta creación fallido, eliminado: ${id}`) devolver respuesta.estado(500).enviar(e) }) .captura(e => respuesta.estado(500).enviar(e)) }) }) .captura(e => respuesta.estado(500).enviar(e)) }) |
Desglosemos este código.
En primer lugar, comprobamos que tanto an correo electrónico
y contraseña
existen en la solicitud.
A continuación, creamos un cuenta
y perfil
en función de los datos enviados en la solicitud. La dirección pid
que estamos guardando en el cuenta
es una clave única. Se establecerá como la clave del documento para nuestro perfil
objeto.
En cuenta utiliza el correo electrónico como clave. En el futuro, si se necesitan otros datos de la cuenta (como correo electrónico alternativo, login social, etc.) podremos asociar otros documentos al perfil.
En lugar de guardar la contraseña en el archivo cuenta
como texto sin formato, le aplicamos un hash con Bcrypt. La contraseña se elimina del perfil
para mayor seguridad. Para más información sobre el hash de contraseñas, consulte este tutorial.
Con los datos listos, podemos insertarlos en Couchbase. El objetivo de este guardado es ser todo o nada. Queremos que tanto el cuenta y perfil se creen con éxito, de lo contrario, se deshará todo. Dependiendo del éxito, devolveremos alguna información al cliente.
Podríamos haber utilizado consultas N1QL para insertar los datos, pero es más fácil utilizar operaciones CRUD sin penalización en el rendimiento.
Uso de testigos de sesión para datos sensibles
Con el perfil de usuario y la cuenta creados, queremos que el usuario inicie sesión y comience a realizar actividades que almacenarán datos y los asociarán a él.
Queremos conectarnos y establecer una sesión que se almacenará en la base de datos haciendo referencia a nuestro perfil de usuario. Con el tiempo, este documento caducará y se eliminará de la base de datos.
El modelo de sesión tendrá el siguiente aspecto:
1 2 3 4 5 |
{ "tipo": "sesión", "id": "ce0875cb-bd27-48eb-b561-beee33c9f405", "pid": "b181551f-071a-4539-96a5-8a3fe8717faf" } |
Este documento, como los demás, tiene una tipo
. Al igual que con el cuenta tiene un pid
que hace referencia a un perfil de usuario.
El código que lo hace posible se encuentra en la sección inicio de sesión punto final:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
aplicación.Correo electrónico:("/login", async (solicitar, respuesta) => { si (!solicitar.cuerpo.correo electrónico) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un `email`" }) } si no si (!solicitar.cuerpo.contraseña) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere una `contraseña`" }) } await colección.consiga(solicitar.cuerpo.correo electrónico) .entonces(async (resultado) => { si (!bcrypt.compareSync(solicitar.cuerpo.contraseña, resultado.valor.contraseña)) { devolver respuesta.estado(500).enviar({ "mensaje": "Contraseña inválida" }) } var sesión = { "tipo": "sesión", "id": uuid.v4(), "pid": resultado.valor.pid } await colección.insertar(sesión.id, sesión, { "caducidad": 3600 }) .entonces(() => respuesta.enviar({ "sid": sesión.id })) .captura(e => respuesta.estado(500).enviar(e)) }) .captura(e => respuesta.estado(500).enviar(e)) }) |
Después de validar los datos entrantes, hacemos una búsqueda de cuentas por dirección de correo electrónico. Si se obtienen datos para el correo electrónico, podemos comparar la contraseña entrante con la contraseña cifrada obtenida en la búsqueda de cuentas. Si esto tiene éxito, podemos crear una nueva sesión para el usuario.
A diferencia de la operación de inserción anterior, establecemos una caducidad del documento de una hora (3600 s). Si no se actualiza la caducidad, el documento desaparecerá. Esto es bueno porque obliga al usuario a registrarse de nuevo y obtener una nueva sesión. Este token de sesión será pasado con cada solicitud futura en lugar de la contraseña.
Gestión de una sesión de usuario con tokens
Queremos obtener información sobre nuestro perfil de usuario, así como asociar nuevas cosas al perfil. Para ello, confirmamos la autoridad a través de la sesión.
Podemos confirmar que la sesión es válida utilizando middleware. Una función Middleware puede ser añadida a cualquier endpoint Express. Esta validación es una función simple que tendrá acceso a la petición HTTP de nuestro endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const valide = async(solicitar, respuesta, siguiente) => { const authHeader = solicitar.cabeceras["autorización"] si (authHeader) { bearerToken = authHeader.dividir(" ") si (bearerToken.longitud == 2) { await colección.consiga(bearerToken[1]) .entonces(async(resultado) => { solicitar.pid = resultado.valor.pid await colección.toque(bearerToken[1], 3600) .entonces(() => siguiente()) .captura((e) => consola.error(e.mensaje)) }) .captura((e) => respuesta.estado(401).enviar({ "mensaje": "Token de sesión inválido" })) } } si no { respuesta.estado(401).enviar({ "mensaje": "Se requiere una cabecera de autorización" }) } } |
Aquí estamos comprobando la solicitud de una cabecera de autorización. Si tenemos un token de portador válido con id de sesión (sid), podemos hacer una búsqueda. El documento de sesión contiene el identificador de perfil. Si la búsqueda de sesión tiene éxito, guardamos el id de perfil (pid) en la petición.
A continuación, refrescamos la expiración de la sesión y nos movemos a través del middleware y de vuelta al endpoint. Si la sesión no existe, no se pasará ningún id de perfil y la solicitud fallará.
Ahora podemos utilizar nuestro middleware para obtener información sobre nuestro perfil en nuestro endpoint de cuenta:
1 2 3 4 5 6 7 8 9 |
aplicación.consiga("/cuenta", valide, async (solicitar, respuesta) => { pruebe { await colección.consiga(solicitar.pid) .entonces((resultado) => respuesta.enviar(resultado.valor)) .captura((e) => respuesta.estado(500).enviar(e)) } captura (e) { consola.error(e.mensaje) } }) |
Fíjese en el valide
y luego el resto de la solicitud. El sitio solicitud.pid
fue establecido por el middleware y nos conseguirá un documento de perfil particular para ese id.
A continuación, creamos un endpoint para añadir un artículo de blog para el usuario:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
aplicación.Correo electrónico:("/blog", valide, async(solicitar, respuesta) => { si(!solicitar.cuerpo.título) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un `título`" }) } si no si(!solicitar.cuerpo.contenido) { devolver respuesta.estado(401).enviar({ "mensaje": "Se requiere un `contenido`" }) } var blog = { "tipo": "blog", "pid": solicitar.pid, "título: solicitar.cuerpo.título, "contenido": solicitar.cuerpo.contenido, "timestamp": (nuevo Fecha()).getTime() } const uniqueId = uuid.v4() colección.insertar(uniqueId, blog) .entonces(() => respuesta.enviar(blog)) .captura((e) => respuesta.estado(500).enviar(e)) }) |
Asumiendo que el middleware ha tenido éxito, creamos un objeto blog con un tipo
y pid
. A continuación, podemos guardarlo en la base de datos.
La consulta de todas las entradas de blog de un usuario concreto no es muy diferente:
1 2 3 4 5 6 7 8 9 10 11 |
aplicación.consiga("/blogs", valide, async(solicitar, respuesta) => { pruebe { const consulta = `SELECCIONAR * DESDE `blog` DONDE tipo = blog Y pid = $PID;` const opciones = { parámetros: { PID: solicitar.pid } } await grupo.consulta(consulta, opciones) .entonces((resultado) => respuesta.enviar(resultado.filas)) .captura((e) => respuesta.estado(500).enviar(e)) } captura (e) { consola.error(e.mensaje) } }) |
Como necesitamos consultar por la propiedad del documento en lugar de por la clave del documento, utilizaremos una consulta N1QL y un índice que creamos previamente.
El documento tipo
y pid
se pasan a la consulta que devuelve todos los documentos de ese perfil concreto.
Conclusión
Acabas de ver cómo crear un almacén y una sesión de perfil de usuario usando Node.js y NoSQL. Este es un gran seguimiento de la explicación de alto nivel de Kirk en su artículo.
Como ya se ha mencionado, el cuenta podrían representar una forma de credenciales de inicio de sesión en la que podrías tener un documento para la autenticación básica (autenticación de Facebook, etc.) haciendo referencia al mismo documento de perfil. En lugar de utilizar un UUID para la sesión, se podría utilizar un JSON Web Token (JWT) o quizás algo más seguro.
En El siguiente tutorial de esta serie nos ayudará a crear un front-end cliente para esta API.
El código terminado, las colecciones de Postman y las variables de entorno disponibles en el archivo couchbaselabs / couchbase-nodejs-blog-api en GitHub.
Para obtener más información sobre el uso de Couchbase con Node.js, consulte la página Portal para desarrolladores de Couchbase.
[...] Enlace: https://www.couchbase.com/creating-user-profile-store-with-node-js-nosql-database/ […]