Простая регистрация, авторизация, аутентификация API на Node.js

Простая регистрация, авторизация, аутентификация API на Node.js

Здесь описана простая регистрация, авторизация и аутентификация API на Node.js, для реальной работы, желательно дополнительно учитывать, что токены должны иметь ограниченный срок действия, также должно быть ограничение на количество попыток аутентификации.

Структура проекта:

models
  db.js
routes
  index.js
app.js

Файл app.js:

const express = require('express');
const bodyParser = require('body-parser');
const router = require('./routes');
const app = express();

const PORT = process.env.PORT || 3000

app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

app.use('/', router);

app.use(function(req, res, next){
  const err = new Error('Ни хрена не найдено!');
  err.status = 404;
  next(err);   
});

app.use(function(err, req, res, next){
  res.status(err.status || 500);
  res.json({
    message: err.message,
    error: err
  })     
})

const server = app.listen(PORT, function () {  
  console.log('Сервер пашет на порту: ' + server.address().port);
})

В этом файле все достаточно стандартно.

Файл db.js:

const MongoClient = require('mongodb').MongoClient;
const url = "строка подключения к базе данных";
const baza = 'test1';

module.exports.getUser = function(email) {
  return new Promise((resolve, reject)=>{
    MongoClient
      .connect(url, function(err, client){
        if (err) {
          reject(err);
        }
        client
          .db(baza)
          .collection('users')
          .find({ "email": email})
          .toArray(function(err, results){
            if (err) {
              reject(err)
            }
            client.close();
            resolve(results);
          })
    })
  })
}

module.exports.getToken = function(token) {
  return new Promise((resolve, reject)=>{
    MongoClient
      .connect(url, function(err, client){
        if (err) {
          reject(err);
        }
        client
          .db(baza)
          .collection('token')
          .find({ "token": token})
          .toArray(function(err, results){
            if (err) {
              reject(err)
            }
            client.close();
            resolve(results);
      })
    })
  })
}

module.exports.add = function(tabl, data) {
  return new Promise((resolve, reject) => {
    MongoClient
      .connect(url, function(err, client) {
        if (err) {
          reject(err);
        }
        client
          .db(baza)
          .collection(tabl)
          .insertOne(data, function(err, results){
            if (err) {
              reject(err);
            }
            client.close();
            resolve(results.ops[0]);
      })
    });         
  })
}

module.exports.delete = function(email) {
  return new Promise((resolve, reject) => {
    //const id = new ObjectID(zadacaId);
    MongoClient
      .connect(url, function(err, client) {
        if (err) {
          reject(err);
        }
        client
          .db(baza)
          .collection('token')
          .deleteMany({ "login": email},
            function(err, results){
              if (err) {
                reject(err);
              }
              client.close();
              resolve(results);
      })            
    });         
  })
}

Этот файл возвращает промиссы взаимодействия с базой данных.

Все самое интересное в файле routes/index.js:

const express = require('express');
const router = express.Router();
const db = require('../models/db');
const bcrypt = require('bcryptjs');
const uuidv4 = require('uuid/v4');

let auth = function(req, res, next) {
  db
    .getToken(req.headers.authorization)
    .then((results)=>{
      if (results.length == 0) {
        const err = new Error('Не авторизован!');
        err.status = 401;
        next(err); 
      } else {
        next()
      }
    })
    .catch((err)=>{
      next(err);
    })
}

const isValidPassword = function(user, password) {
  return bcrypt.compareSync(password, user.password);
}

router.get('/', (req, res)=>{
  res.json({
    message: 'Добро пожаловать!'
  })       
});

router.get('/secret', auth, (req, res)=>{
  res.json({
    message: 'Секретная страница!'
  })   
});

router.post('/registration', (req, res, next)=>{
  if(req.body.password === req.body.repeatPassword){
    db
      .getUser(req.body.email)
      .then((results)=>{
        if (results.length == 0){
          data = {
            email: req.body.email,
            password: bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null)
          };
          db
            .add('users', data)
            .then((results)=>{
              res.json({
                message: 'Пользователь добавлен: ' + results[0]
              })
            })
            .catch((err)=>{
              next(err);
            })
        } else {
          const err = new Error('Такой пользователь уже есть!');
          err.status = 400;
            next(err);
        }
      })
      .catch((err)=>{
        next(err);
      })
  } else {
    const err = new Error('Не совпадает пароль и подтверждение пароля!');
    err.status = 400;
      next(err);        
  }
})

router.post('/login', (req, res, next)=>{
  db
    .getUser(req.body.email)
    .then((results)=>{
      if (isValidPassword(results[0], req.body.password)) {
        data ={};
        data.login=req.body.email;
        data.token=uuidv4();
        db
          .delete(req.body.email)
          .then((results)=>{
            db
              .add('token', data)
              .then((results)=>{
                res.json({
                  token: results.token
                })                            
              })
              .catch((err)=>{
                next(err)
              })
          })
          .catch((err)=>{
            next(err)
          })
      } else {
        const err = new Error('Не верный логин или пароль!');
        err.status = 400;
        next(err); 
      }
    })
    .catch((err)=>{
      next(err);
    })
})

module.exports = router;

Давайте рассмотрим подробнее, что здесь делаем:

Подключаем модуль ‘bcryptjs’ – он необходим для шифрования пароля, так как в базе данных нужно хранить пароль в зашифрованном виде.

Подключаем модуль ‘uuid/v4’ – для генерации рандомного токена.

Регистрация

router.post('/registration', (req, res, next)=>{
  if(req.body.password === req.body.repeatPassword){
    db
      .getUser(req.body.email)
      .then((results)=>{
        if (results.length == 0){
          data = {
            email: req.body.email,
            password: bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null)
          };
          db
            .add('users', data)
            .then((results)=>{
              res.json({
                message: 'Пользователь добавлен: ' + results[0]
              })
            })
            .catch((err)=>{
              next(err);
            })
        } else {
          const err = new Error('Такой пользователь уже есть!');
          err.status = 400;
            next(err);
        }
      })
      .catch((err)=>{
        next(err);
      })
  } else {
    const err = new Error('Не совпадает пароль и подтверждение пароля!');
    err.status = 400;
      next(err);        
  }
})

Регистрация от нас ожидает post запрос, в теле (body) которого три поля: email, password, repeatPassword

Дальше сравниваем совпадает ли поля password и repeatPassword, если не совпадают генирим ошибку и пробрасываем ее дальше.

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

Если все хорошо, то добавляем пользователя в базу данных, зашифровав пароль:

bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10), null)

Авторизация пользователя

router.post('/login', (req, res, next)=>{
  db
    .getUser(req.body.email)
    .then((results)=>{
      if (isValidPassword(results[0], req.body.password)) {
        data ={};
        data.login=req.body.email;
        data.token=uuidv4();
        db
          .delete(req.body.email)
          .then((results)=>{
            db
              .add('token', data)
              .then((results)=>{
                res.json({
                  token: results.token
                })                            
              })
              .catch((err)=>{
                next(err)
              })
          })
          .catch((err)=>{
            next(err)
          })
      } else {
        const err = new Error('Не верный логин или пароль!');
        err.status = 400;
        next(err); 
      }
    })
    .catch((err)=>{
      next(err);
    })
})

Этот роут ожидает от нас post запрос с двумя полями email и password. В случае успешного завершения возвращает “токен” иначе ошибку.

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

Если находим, то проверяем пароль:

if (isValidPassword(results[0], req.body.password)) {

Функция проверки пароля:

const isValidPassword = function(user, password) {
  return bcrypt.compareSync(password, user.password);
}

Если пароль верный генерируем токен:

data.token=uuidv4();

Далее очищаем предыдущие токены, которые были в базе данных по этому пользователю и записываем новый.

Если все хорошо, то возвращаем токен пользователю.

Открытый роут

router.get('/', (req, res)=>{
  res.json({
    message: 'Добро пожаловать!'
  })       
});

При обращении на localhost:3000, нам вернется всегда ‘Добро пожаловать’, так как это открытый роутер и мы его ничем не закрывали.

Закрытый роут

router.get('/secret', auth, (req, res)=>{
  res.json({
    message: 'Секретная страница!'
  })   
});

Отличие лишь в том, что здесь добавлен миделвар auth – который проверяет можно ли пользователю просматривать страницу, и который сравнивает в headers запросе параметр ‘Authorization’ со значением в базе данных, если находит в базе данных токен, тогда разрешает, иначе возвращает ошибку ‘Не авторизован’:

let auth = function(req, res, next) {
  db
    .getToken(req.headers.authorization)
    .then((results)=>{
      if (results.length == 0) {
        const err = new Error('Не авторизован!');
        err.status = 401;
        next(err); 
      } else {
        next()
      }
    })
    .catch((err)=>{
      next(err);
    })
}

Вроде все просто!