Эта статья является шестой из серии статей, которая поможет вам понять различные концепции, лежащие в основе Node.js, и даст вам возможность создавать готовые к эксплуатации приложения. В этой статье предполагается, что читатель знает Babel и то, как его настроить. Пожалуйста, прочтите эту статью, если вам нужно узнать то же самое.

Когда вы создаете бэкэнд для своего веб-сайта / веб-приложения или общий бэкэнд, который будет действовать как бэкэнд как для мобильных, так и для веб-приложений, вам необходимо защитить свои API с помощью уровня аутентификации. Эти API должны быть доступны только зарегистрированному пользователю и должны возвращать результат в зависимости от пользователя.

В эту современную эпоху все используют аутентификацию на основе токенов вместо файлов cookie на основе сеанса. OAuth, JWT и случайный токен с memcached / redis являются наиболее популярными формами аутентификации на основе токенов.
Есть хорошая таблица, в которой сравниваются некоторые из распространенных методов аутентификации:



Отказ от ответственности: Электронная таблица была написана другим автором.

И JWT, и случайный токен с хранением в памяти имеют свои преимущества и недостатки. Вы должны использовать их в соответствии с требованиями проекта. Вы можете найти в статье ниже, в которой они оба сравниваются:



Что касается redis и memcached, я предпочитаю redis по следующим причинам:

а) Поддержка большего количества типов данных

б) Постоянство диска по умолчанию

в) Больший размер значения ключа

Подробнее читайте в статье ниже:

Https://www.linkedin.com/pulse/memcached-vs-redis-which-one-pick-ranjeet-vimal/

Теперь, когда мы решили перейти на аутентификацию на основе токенов с помощью redis, приступим.

Redis - это хранилище структур данных в памяти с открытым исходным кодом (под лицензией BSD), используемое в качестве базы данных, кеша и посредника сообщений. Он поддерживает такие структуры данных, как строки, хэши, списки, наборы, отсортированные наборы с запросами диапазона, растровые изображения и т. Д. Два самых популярных использования redis - это создание уровня кэширования или работа в качестве системы хранения сеансов. .

Пожалуйста, ознакомьтесь со следующими статьями, чтобы начать работу с Redis.







В этой статье мы будем использовать локальную установку Redis. Но вы также можете получить бесплатный сервер в лабораториях Redis.



Начнем с того места, где мы остановились в статье №5. Перед прочтением этой статьи рекомендуется прочитать 5-ю статью. Но если у вас есть некоторый опыт работы с node.js и вы можете найти свой собственный путь через код, вы можете начать с шаблона ниже:

Вы можете найти код здесь:

Https://github.com/pankaj805/medium-05_mongo_client

Мы будем использовать следующий модуль npm в качестве клиентской библиотеки redis в нашем проекте.



Итак, давайте сначала установим в наш проект следующие библиотеки.

npm install redis --save

и еще одна библиотека для создания случайных уникальных идентификаторов

npm install node-uuid --save

В этом руководстве мы создадим два api: первый api - это api «логин», который должен возвращать новый токен сеанса, а другой api будет обновлять пароль, для которого потребуется действительный токен сеанса зарегистрированного пользователя.

давайте сначала добавим сервисный метод для обновления пароля пользователя

export const updateUserPassword = (db, userName,pwd) => {
  return db.collection('user').updateOne({'username': userName }, {
    $set: {password:pwd} 
  })
  .then((r) => {
    return Promise.resolve(r.matchedCount);
  })
  .catch((err) => {
    return Promise.reject(err);
  })
}

Итак, наконец, services / UserService.js должен выглядеть так:

export const getUserDetails = (db, userName) => {
   return new Promise((resolve, reject) =>
      db.collection('user')
        .find({ 'username': userName })
        .toArray((err, docs) => {
           if(docs && docs.length>0){
              resolve(docs[0]);
           }else{
              reject();
           }
      });
   });
}
export const updateUserPassword = (db, userName,pwd) => {
  return db.collection('user').updateOne({'username': userName }, {
    $set: {password:pwd} 
  })
  .then((r) => {
    return Promise.resolve(r.matchedCount);
  })
  .catch((err) => {
    return Promise.reject(err);
  })
}

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

class Session {
  constructor() {
    this.userData = {};
    this.sessionID = '';
  }
 
  set(obj) {
    this.userData = obj;
  }
  save(client){
    if(this.sessionID){
      client.set(this.sessionID, JSON.stringify(this.userData));
      client.expire(this.sessionID, 60 * 60 * 2);
    }
  }
  destroy(client) {
    client.del(this.sessionID);
  }
}
export default Session;

Этот файл не требует пояснений, за исключением методов сохранения и уничтожения. Методу сохранения передается один аргумент, который должен быть ссылкой на экземпляр клиента Redis.

client.set(this.sessionID, JSON.stringify(this.userData));

Вышеупомянутая строка используется для обновления пары ключ-значение redis пользовательскими данными.

client.expire(this.sessionID, 60 * 60 * 2);

а client.del () - удалить определенную пару "ключ-значение" из redis.

Теперь давайте отредактируем наш общий файл / authUtils.js для обработки желаемых сценариев.

import uuid from 'node-uuid';
const newSessionRoutes = [{ path: '/user/login', method: 'POST' }];
const authRoutes = [{ path: '/user/password', method: 'PUT' }];

Импортируйте модуль uuid и определите две константы: первая содержит массив методов api, которым требуется новый сеанс, а вторая - массив методов api, которым требуется действующий сеанс.

export const isNewSessionRequired = (httpMethod, url) => {
  for (let routeObj of newSessionRoutes) {
    if (routeObj.method === httpMethod && routeObj.path === url) {
      return true;
    }
  }
  return false;
}
export const isAuthRequired = (httpMethod, url) => {
  for (let routeObj of authRoutes) {
    if (routeObj.method === httpMethod && routeObj.path === url) {
      return true;
    }
  }
  return false;
}

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

export const generateRandomSessionID = () => {
  return uuid.v4();
}

Добавьте указанный выше метод для создания случайной строки uuid. Мы будем использовать его для создания уникального ключа для нашей пары ключ-значение redis.

Нам также нужен метод для чтения значения с сервера Redis. Согласно документации нам необходимо сделать следующее:

client.get("redis-key", function (err, reply) {
  console.log(reply.toString());
});

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

export const getRedisSessionData = (redisClient,sessionId) => {
  return new Promise((resolve, reject) => {
    redisClient.get(sessionId, function (err, data) {
      if (err) {
        reject(err);
      }
      resolve(data);
    });
  })
}

Итак, наш файл authUtils должен выглядеть так:

import {getClientDetails} from '../services/ClientService';
import uuid from 'node-uuid';
const newSessionRoutes = [{ path: '/user/login', method: 'POST' }];
const authRoutes = [{ path: '/user/password', method: 'PUT' }];
export const clientApiKeyValidation = async (req,res,next) => {
   let clientApiKey = req.get('api_key');
if(!clientApiKey){
      return res.status(400).send({
         status:false,
         response:"Missing Api Key"
      });
   }
try {
      let clientDetails = await getClientDetails(req.db, clientApiKey);
      if (clientDetails) {
         next();
      }
   } catch (e) {
      console.log('%%%%%%%% error :', e);
      return res.status(400).send({
         status: false,
         response: "Invalid Api Key"
      });
   }
}
export const isNewSessionRequired = (httpMethod, url) => {
  for (let routeObj of newSessionRoutes) {
    if (routeObj.method === httpMethod && routeObj.path === url) {
      return true;
    }
  }
return false;
}
export const isAuthRequired = (httpMethod, url) => {
  for (let routeObj of authRoutes) {
    if (routeObj.method === httpMethod && routeObj.path === url) {
      return true;
    }
  }
  return false;
}
export const generateRandomSessionID = () => {
  return uuid.v4();
}
export const getRedisSessionData = (redisClient,sessionId) => {
  return new Promise((resolve, reject) => {
    redisClient.get(sessionId, function (err, data) {
      if (err) {
        reject(err);
      }
      resolve(data);
    });
  })
}

Перед тем, как перейти к следующему файлу, сделаем несколько примечаний:

Это не код качества продукции. Мы слишком упростили вещи и проигнорировали несколько лучших практик. Вот список вещей, которые слишком упрощены, чтобы ограничить фокус статьи:

i) конфигурация авторизации apis помещена в постоянные переменные. В идеале они должны быть получены из базы данных или должны быть помещены в файл json. Даже конфигурация может быть более сложной для обработки аутентификации на основе ролей пользователя.

ii) Пароли должны храниться в зашифрованном виде. Этого легко добиться, используя различные библиотеки npm, такие как bcrypt и т. Д.

Теперь давайте отредактируем файл app.js.

import {clientApiKeyValidation} from './common/authUtils';

Обновите строку выше, как показано ниже:

import { clientApiKeyValidation, isNewSessionRequired, isAuthRequired, generateRandomSessionID, getRedisSessionData } from './common/authUtils';

И еще два импорта

import redis from 'redis';
import Session from './common/Session';

Добавьте следующий код после / перед кодом подключения mongo.

let redisClient = null;
redisClient = redis.createClient({
  prefix: 'node-sess:',
  host: 'localhost'
});

Мы создаем экземпляр клиентского соединения Redis и назначаем его глобальной переменной. Значение префикса будет использоваться во всех ключах, записываемых / считываемых этим клиентом. По умолчанию используется порт 6379.

Теперь добавьте следующий код сразу после строки, в которую мы ввели clientApiKeyValidation.

app.use(async (req, res, next) => {
  var apiUrl = req.originalUrl;
  var httpMethod = req.method;
if (isNewSessionRequired(httpMethod, apiUrl)) {
    let sessionID = generateRandomSessionID()
    req.session = new Session();
    req.session.sessionID = sessionID;
    req.sessionID = sessionID;
  } else if (isAuthRequired(httpMethod, apiUrl)) {
    let sessionID = req.header('Authorization');
    if (sessionID) {
      let redisData = await getRedisSessionData(redisClient, sessionID);
      if (redisData) {
        redisData = JSON.parse(redisData);
        req.session = new Session();
        req.sessionID = sessionID;
        req.session.sessionID = sessionID;
        req.session.userData = redisData;
      } else {
        return res.status(401).send({
          ok: false,
          error: {
            reason: "Invalid Sessiontoken",
            code: 401
          }
        });
      }
    } else {
      return res.status(401).send({
        ok: false,
        error: {
          reason: "Missing Sessiontoken",
          code: 401
        }
      });
    }
  }
  next();
})

С помощью приведенного выше кода мы вводим еще один обработчик запросов, который будет обрабатывать связанные с сеансом вещи для всех API. Сначала мы проверяем, требуется ли новый сеанс для вызванного api. В этом случае мы создаем случайный uuid и новый объект сеанса и назначаем его как свойство объекта экспресс-запроса. Но мы еще не отправили эти данные на наш сервер Redis. Мы сделаем это позже.

let sessionID = generateRandomSessionID()
req.session = new Session();
req.session.sessionID = sessionID;
req.sessionID = sessionID;

В другом условии мы проверяем, нужен ли api действующий сеанс. Здесь очень важное замечание. Обычно заголовок токена авторизации передается следующим образом:

Authorization: Bearer a1194981-b541-416e-935

Поле заголовка должно быть «Авторизация», и первая часть значения должна быть ключевым словом «Носитель», а затем вторая часть, разделенная пробелом, должна содержать токен.

Это стандарт, разработанный W3C. Ключевое слово «Bearer» использовалось, чтобы отличить это от существующей базовой аутентификации. Но в этой статье мы будем использовать настраиваемый заголовок, чтобы продемонстрировать, что мы также можем сделать это с помощью настраиваемого поля заголовка. В следующей статье мы будем использовать заголовок Bearer Authorization.

Таким образом, мы ожидаем, что клиент отправит токен сеанса в заголовке запроса с именем поля sessiontoken.

let sessionID = req.header('sessiontoken');

Если идентификатор сеанса отсутствует, мы возвращаем ошибку в api. Но если в заголовке присутствует идентификатор сеанса, мы пытаемся подключиться к серверу redis и получить запись для переданного ключа.

let redisData = await getRedisSessionData(redisClient, sessionID);

Если redisData имеет значение null, мы возвращаем ошибку в API, в которой говорится, что токен недействителен. Это может произойти, если срок действия сеанса истек и он был удален с сервера Redis, или если в заголовке было передано случайное недопустимое значение.

Но если мы получим данные пользователя из redis. Разбираем одни и те же данные. {Мы будем хранить наши данные в виде строки в redis}. Мы делаем то же самое, что и для нового сеанса, за исключением того, что сохраняем старые значения.

        redisData = JSON.parse(redisData);
        req.session = new Session();
        req.sessionID = sessionID;
        req.session.sessionID = sessionID;
        req.session.userData = redisData;

Теперь, после всех инъекций маршрутов, давайте добавим обработчик ответа для всех наших API.

app.use((req, res, next) => {
  if (!res.data) {
    return res.status(404).send({
      ok: false,
      error: {
        reason: "Invalid Endpoint", code: 404
      }
    });
  }
  if (req.session && req.sessionID) {
    try {
      req.session.save(redisClient);
      res.setHeader('sessiontoken', req.sessionID);
      res.data['sessiontoken'] = req.sessionID;
    } catch (e) {
      console.log('Error ->:', e);
    }
  }
  res.status(res.statusCode || 200)
    .send({ ok: true, response: res.data });
})

В этом обработчике ответа. Сначала мы проверяем, имеет ли объект ответа свойство data. Если свойство data отсутствует, мы отправили ошибку 404 в нашем ответе api.

Но если данные установлены, мы отправляем ответ в определенном формате с требуемым кодом статуса http.

res.status(res.statusCode||200).send({ok: true, response:res.data});

И если у объекта экспресс-запроса есть поле sessionID, мы выполняем два действия:

i) Сначала мы сохраняем эти данные на нашем сервере redis с помощью метода Session.save ().

ii) Мы добавляем значение токена сеанса как в заголовок ответа, так и в полезную нагрузку ответа.

Итак, наш код app.js выглядит так:

import express from 'express';
import bodyParser from 'body-parser';
import user from './routes/user';
import {MongoClient} from 'mongodb';
import { clientApiKeyValidation, isNewSessionRequired, isAuthRequired, generateRandomSessionID, getRedisSessionData } from './common/authUtils';
import redis from 'redis';
import Session from './common/Session';
const CONN_URL = 'mongodb://localhost:27017';
let mongoClient = null;
MongoClient.connect(CONN_URL,{ useNewUrlParser: true }, function (err, client) {
   mongoClient = client;
})
let redisClient = null;
redisClient = redis.createClient({
  prefix: 'node-sess:',
  host: 'localhost'
});
let app = express();
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());
app.use((req,res,next)=>{
   req.db = mongoClient.db('test');
   next();
})
app.get('/',(req,res,next)=>{
    res.status(200).send({
      status:true,
      response:'Hello World!'
    });
});
app.use(clientApiKeyValidation);
app.use(async (req, res, next) => {
  var apiUrl = req.originalUrl;
  var httpMethod = req.method;
if (isNewSessionRequired(httpMethod, apiUrl)) {
    let sessionID = generateRandomSessionID()
    req.session = new Session();
    req.session.sessionID = sessionID;
    req.sessionID = sessionID;
  } else if (isAuthRequired(httpMethod, apiUrl)) {
    let sessionID = req.header('sessiontoken');
    if (sessionID) {
      let redisData = await getRedisSessionData(redisClient, sessionID);
      if (redisData) {
        redisData = JSON.parse(redisData);
        req.session = new Session();
        req.sessionID = sessionID;
        req.session.sessionID = sessionID;
        req.session.userData = redisData;
      } else {
        return res.status(401).send({
          ok: false,
          error: {
            reason: "Invalid Sessiontoken",
            code: 401
          }
        });
      }
    } else {
      return res.status(401).send({
        ok: false,
        error: {
          reason: "Missing Sessiontoken",
          code: 401
        }
      });
    }
  }
  next();
})
app.use('/user',user);
app.use((req, res, next) => {
  if (!res.data) {
    return res.status(404).send({
      ok: false,
      error: {
        reason: "Invalid Endpoint", code: 404
      }
    });
  }
if (req.session && req.sessionID) {
    try {
      req.session.save(redisClient);
      res.setHeader('sessiontoken', req.sessionID);
      res.data['sessiontoken'] = req.sessionID;
    } catch (e) {
      console.log('Error ->:', e);
    }
  }
  res.status(res.statusCode || 200)
    .send({ ok: true, response: res.data });
})
app.listen(30006,()=>{
   console.log(' ********** : running on 30006');
})
process.on('exit', (code) => {
   mongoClient.close();
   console.log(`About to exit with code: ${code}`);
});
process.on('SIGINT', function() {
   console.log("Caught interrupt signal");
   process.exit();
});
module.exports = app;

Теперь перейдем к нашему файлу routes / user.js. Перед добавлением двух новых маршрутов api давайте отредактируем предыдущий маршрут «/ hello» для работы с новым обработчиком ответа.

Раньше код был таким:

router.post('/hello',async (req,res)=>{
  let uname = req.body.username;
  let userDetails = await getUserDetails(req.db,uname)
  res.status(200).send({
    status:true,
    response:userDetails
  });
});

Здесь при успешном извлечении данных мы использовали прямую отправку ответа. Теперь давайте отредактируем приведенный выше метод на следующий:

router.post('/hello', async (req, res, next) => {
  let uname = req.body.username;
  let userDetails = await getUserDetails(req.db, uname)
  res.data = {
    status: true,
    response: userDetails
  };
  next();
});

Здесь мы просто устанавливаем атрибут данных объекта ответа и вызываем метод next ().

Обновите оператор импорта, чтобы включить метод updateUserPassword.

import { getUserDetails, updateUserPassword } from '../services/UserService';

Код для входа в систему api выглядит следующим образом:

router.post('/login', async (req, res, next) => {
  let uname = req.body.username;
  let pwd = req.body.password;
  let userDetails = await getUserDetails(req.db, uname);
  if (userDetails) {
    let { password } = userDetails;
    if (pwd === password) {
      res.data = {
        status: true,
        response: userDetails
      };
      req.session.set(userDetails);
    } else {
      res.statusCode = 400;
      res.data = {
        status: false,
        error: 'Invalid Password'
      };
    }
  } else {
    res.statusCode = 400;
    res.data = {
      status: false,
      error: 'Invalid Username'
    };
  }
  next();
});

Здесь мы также можем добавить еще одно условие, чтобы проверить, отсутствуют ли имя пользователя или пароль в данных запроса. Сначала мы пытаемся получить данные пользователя с переданным именем пользователя. В случае отсутствия данных мы отправляем ошибку с указанием Неверное имя пользователя. В противном случае мы пытаемся сравнить пароли. В случае несоответствия пароля мы отправляем ошибку. В противном случае мы обновляем данные пользователя как в объекте данных ответа, так и в объекте сеанса запроса. Поскольку объект сеанса запроса теперь имеет пользовательские данные, функция save (), которая вызывается в нашем обработчике ответа, сохранит данные в нашем Redis.

Теперь давайте добавим код для обновления пароля api.

router.put('/password', async (req, res, next) => {
  try {
    let oldPwd = req.body.old_password;
    let newPwd = req.body.new_password;
    if (!oldPwd && !newPwd) {
      res.statusCode = 400;
      res.data = {
        status: false,
        error: 'Invalid Parameters'
      }
    }
    let uname = req.session.userData.username;
    let userDetails = await getUserDetails(req.db, uname);
    if (oldPwd !== userDetails.password) {
      res.statusCode = 400;
      res.data = {
        status: false,
        error: "Old Password doesn't match"
      }
    } else {
      let updateRes = await updateUserPassword(req.db,uname,newPwd)
      res.data = {
        status: true,
        response:updateRes,
        message: "Password updated successfully"
      }
    }  
    next();
  } catch (e) {
    next(e)
  }
})

Это интересная часть кода. Здесь мы увидим, как получить все данные о пользователе из токена сеанса. Данные пользователя или имя пользователя нигде в запросе не упоминались. Но наш обработчик сеанса, который мы внедрили как обработчик запроса, должен был внедрить пользовательские данные в наш объект сеанса запроса.

let uname = req.session.userData.username;

Мы получаем имя пользователя из объекта сеанса запроса, а затем выполняем все действия, необходимые для обновления пароля пользователя.

Уф !!!!!

Теперь давайте протестируем наш код. Запустите проект с помощью команды node index.js.

Давайте сначала коснемся нашего старого api «/ hello», чтобы проверить наш новый обработчик ответа.

Давайте теперь нажмем на api входа в систему:

Мы можем увидеть токен сеанса в ответе api. Мы также можем проверить экземпляр Redis, чтобы убедиться, что у нас есть данные пользователя.

Теперь давайте попробуем использовать api для обновления пароля с недопустимым токеном:

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

Надеюсь, вам понравилась статья, и в следующей статье мы увидим, как реализовать аутентификацию с помощью JWT.

Вы можете найти код здесь:

Https://github.com/pankaj805/medium-06_redis

Если вам понравилась статья, вы можете 👏 рассказать историю и поделиться ею с другими. Вы можете найти всю серию статей здесь.