Recentemente escrevi um tutorial ensinando como utilizar o Passport, uma popular lib JavaScript para construção de middlewares de autenticação. Neste tutorial, fiz toda a gestão de sessão e de autenticação em memória, usando um array simples, para fins didáticos.
A gestão de sessão não é incomum ser feita em memória, mas possivelmente você irá querer a gestão dos seus usuários persistida em um banco de dados, certo?
No tutorial de hoje, vamos adaptar o projeto anterior (então baixe ele na sua máquina) para uso com MongoDB, um popular banco de dados NoSQL.
Atenção: este é um tutorial intermediário. Parto do pressuposto aqui que você já conhece Node.js e MongoDB em níveis básicos e também que já realizou o tutorial anterior, onde criamos este projeto com persistência em array.
#1 – Configurando o banco de dados e variáveis de ambiente
Os dados de sessão dos usuários e os próprios usuários vão ficar armazenados em um banco MongoDB, substituindo o array do tutorial anterior.
Eu explico tudo que você precisa saber sobre como configurar o ambiente MongoDB pra uso com Node no vídeo abaixo. Caso não esteja com tempo agora, sugiro apenas que crie uma conta na MongoDB Atlas, que tem um plano gratuito.
Voltando ao nosso projeto Node.js, instale o driver do MongoDB para Node:
1 2 3 |
npm i mongodb dotenv-safe |
Note que instalei também o pacote dotenv-safe, que permitirá guardarmos variáveis de ambiente de uma maneira muito prática e profissional, permitindo fazer deploy do nosso projeto em produção sem ter de ficar trocando connection strings e coisas do tipo.
Para que o dotenv-safe funcione (ensino em videoaulas do meu curso de Node.js e MongoDB), você precisa criar dois arquivos novos: “.env” e “.env.example”. O “.env.example” é o template contendo quais variáveis de ambiente são obrigatórias para sua aplicação funcionar, em nosso caso, apenas três:
1 2 3 4 5 6 |
#.env.example commit to repo MONGO_CONNECTION= MONGO_DB= SESSION_SECRET= |
Já o “.env”, contém a sua versão local das variáveis e o ideal é que você inclua no seu .gitignore o .env, para que ele não seja commitado em produção (em produção você deve setar as variáveis de ambiente de produção, e não vai querer elas sobrescritas de maneira alguma):
1 2 3 4 5 6 |
#.env DON'T commit to repo MONGO_CONNECTION=mongodb://127.0.0.1:27017/passport MONGO_DB=passport SESSION_SECRET=123 |
Agora modifique o seu bin/www (geramos este projeto com o express-generator, lembra?) para que o servidor somente seja iniciado após a conexão com o banco de dados tiver sido realizada, como abaixo, onde uso a variável de ambiente como connection string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
(async () => { require('dotenv-safe').config(); const app = require('../app'); const debug = require('debug')('nodejs-passport-mongodb:server'); const http = require('http'); const mongoClient = require("mongodb").MongoClient; try { const conn = await mongoClient.connect(process.env.MONGO_CONNECTION); console.log("conectou no banco de dados!"); global.db = conn.db(process.env.MONGO_DB); } catch (err) { return console.log(err); } //daqui pra baixo não mexi em nada ... })(); |
Repare que eu encapsulei todo o conteúdo do bin/www em um IIFE. Isso foi necessário para conseguir usar async/await na conexão com o banco.
O objeto db é a conexão com o banco e vamos compartilhá-lo globalmente com os outros módulos que precisarem (apontando diretamente para o banco configurado na variável de ambiente). Caso o .env não exista, teremos um erro. Caso não consiga se conectar ao banco, teremos um erro. O projeto somente vai “subir” se estiver tudo 100%.
Se você rodar este projeto agora e tudo estiver 100%, você deve ver a mensagem no console de que a conexão foi bem sucedida.
Antes de prosseguirmos, é de bom tom cadastrarmos no mínimo um usuário em nosso banco de dados, para poder realizar os testes depois. Acesse o seu banco MongoDB local ou remoto e insira no banco passport, na coleção users, um usuário com os seguintes dados:
1 2 3 |
db.users.insert({username: "adm", password: "$2a$06$HT.EmXYUUhNo3UQMl9APmeC0SwoGsx7FtMoAWdzGicZJ4wR1J8alW", email: "[email protected]"}); |
Coloque no campo email, um email seu de verdade, pois precisaremos dele funcionando mais tarde. Note que o password do usuário está criptografado em formato de hash Bcrypt. Esta senha na verdade é o hash da palavra “123”. 🙂
#2 – Refatorando o app.js
Antes de mais nada, no nosso app.js nós definimos um secret que é usado pelo Passport para criptografar algumas coisas dele. Hoje este secret está chumbado, mas vamos mudar isso para usar uma variável de ambiente, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 |
require('./auth')(passport); app.use(session({ secret: process.env.SESSION_SECRET,//configure um segredo seu aqui, resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 60 * 1000 }//30min })) app.use(passport.initialize()); app.use(passport.session()); |
Além disso, vamos instalar uma nova dependência no projeto para que o express-session use o MongoDB ao invés da memória para gerenciar as sessões. O express-session é muito extensível e basta instalarmos o conector para o banco de dados apropriado e fazer umas poucas configurações.
1 2 3 |
npm i connect-mongo |
No app.js, no mesmo bloco de configuração do Passport, carregue o middleware de sessão do connect-mongo e adicione-o na chamada do session, para que ele passe a apontar para o MongoDB corretamente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const MongoStore = require('connect-mongo'); require('./auth')(passport); app.use(session({ store: MongoStore.create({ autoRemove: 'native', mongoUrl: process.env.MONGO_CONNECTION, dbName: process.env.MONGO_DB, ttl: 30 * 60 // = 30 minutos de sessão }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 60 * 1000 }//30min })) app.use(passport.initialize()); app.use(passport.session()); |
Aqui estou carregando nosso módulo auth.js passando o objeto passport pra ele configurar a estratégia de autenticação. Depois digo para o express-session usar a mesma string de conexão com o MongoDB e que os dados de sessão devem ser apagados automaticamente após 30 minutos, um recurso nativo do MongoDB chamando TTL Index.
#3 – Refatorando o auth.js
Agora que temos nosso banco rodando, vamos refatorar as partes do projeto que usavam o array em memória para fazer a gestão dos usuários, abrindo nosso auth.js e antes de tudo, removendo o array de users e depois refatorando as duas funções de findUser como abaixo.
1 2 3 4 5 6 7 8 9 10 |
function findUser(username) { return global.db.collection("users").findOne({ "username": username }); } function findUserById(id) { const ObjectId = require("mongodb").ObjectId; return global.db.collection("users").findOne({ _id: ObjectId.createFromHexString(id) }); } |
Assim, ao invés de estarmos procurando em um array, estaremos procurando diretamente no banco de dados.
Como as funções do MongoDB são assíncronas, ou usamos callbacks, promises ou ainda async/await, que foi a opção que escolhi aqui. Devido a isso, temos de alterar as funções que chamam as findUser para que usem async/await:
1 2 3 4 5 6 7 8 9 10 |
passport.deserializeUser(async (id, done) => { try { const user = await findUserById(id); done(null, user); } catch (err) { done(err, null); } }); |
e essa aqui também:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password' }, async (username, password, done) => { try { const user = await findUser(username); // usuário inexistente if (!user) { return done(null, false) } // comparando as senhas const isValid = bcrypt.compareSync(password, user.password); if (!isValid) return done(null, false); return done(null, user); } catch (err) { done(err, false); } } )); |
E com isso você tem novamente a sua aplicação web em Node.js funcionando e exigindo autenticação, mas agora com o MongoDB como persistência de dados!
Confira a parte 2 deste tutorial neste link.
Quer aprender a construir aplicações web usando Node.js e MongoDB com a mesma didática deste tutorial? Conheça o meu curso online ou meu livro.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.