
Эта статья является шестой из серии статей, которая поможет вам понять различные концепции, лежащие в основе 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 и вы можете найти свой собственный путь через код, вы можете начать с шаблона ниже:
Вы можете найти код здесь:
Мы будем использовать следующий модуль 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.
Вы можете найти код здесь:
Если вам понравилась статья, вы можете 👏 рассказать историю и поделиться ею с другими. Вы можете найти всю серию статей здесь.