Nodejs авторизация через passport шаг за шагом

Кроме PHP в круг моих интересов входит так же и nodejs. Разобрался как работает Express, с помощью книги «Веб-разработка с применением Node и Express», но казалось бы, авторизацию там рассмотрели из рук вон плохо. Объяснили что такое Passport но не конкретно, и то на примере Passport-facebook. Зачем они так сделали, совершенно не понятно. Поэтому будем разбираться сами.

Пришлось перерыть кучу сайтов, чтобы найти информацию и разобраться что к чему, чтобы сублимировать все знания, пишу здесь.

Итак, давайте начнем сначала, у нас есть Express. Это библиотека, которая обрабатывает Web-запросы.

npm install express


const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello world");
});

app.listen(process.env.PORT || 3000, () => {
  console.log("server start...");
});

Если вдруг тут будут начинающие, пару слов что тут есть. экспортируем функцию express, создаем объект app. в Javascript вот такой вот удивительный ООП. Но это сильно лучше чем во многих других языках если честно.

Дальше, вызываем функцию .get у объекта app, регистрируя обработчик маршрута / или можно сказать мы написали контроллер, который ждет / т.е. мы будем обрабатывать такой запрос http://localhost:3000/

ну и в конце запускаем сервер, который будет прослушивать указанный порт и запускать наши контроллеры, которые мы описали.

Итак, для того чтобы сделать авторизацию нам нужно импортировать сам passport и библиотеку для работы с сессиями.

npm install passport
npm install express-session

Кроме того passport интересно сделали, через расширяемые библиотеки, это называется стратегией, т.е. сама эта библиотека работает с авторизацией, а вот каким образом мы будем авторизовывать пользователей уже выбираем через какой то другой, назовем его плагином, который определяет стратегию. Т.е. библиотека passport работает с плагинами и они уже определяют способ авторизации. Например это может быть локальная авторизация, мы можем авторизовывать через локальную базу данных, или как угодно, т.е. самостоятельно. Этим занимается плагин passport-local, можем захотеть авторизовывать пользователей через google, этим занимается плагин passport-google-oauth, есть куча других плагинов, там их больше 500.

npm install passport-local

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

const express = require("express");
const app = express();

const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

app.get("/", (req, res) => {
  res.send("Hello world");
});

app.listen(process.env.PORT || 3000, () => {
  console.log("server start...");
});

Итак, мы импортировали session, для работы с сессиями. Саму passport и локальную стратегию, т.е. плагин, через который мы будем проверять самостоятельно и говорить passport’у что пользователь авторизован или нет.

Итак, что такое session. По простому представьте себе ассоциативный массив где то в памяти. Обычный объект в JavaScript.

const session = {};

Например пользователь делает запрос на сервер http://localhost:3000/

сессия что делает, она создает какой то Hash длинный, это может быть что-то другое, я сейчас упрощенно, и вешает пользователю Cookie с этим идентификатором,

Итак, схемка примерно такая, т.е. идет первый запрос, Cookie пустые, Express-session создает в каком то своем глобальном объекте sessions={} объект с уникальным хешем в качестве ключа, скажем sessions[‘sdfadfasdfasdf’]={}

Дальше записывает в Cookie этот идентификатор sdfadfasdfasdf и отправляет его пользователю в браузер вместе с ответом.

Второй запрос уже на сервер прилетает с Cookie в котором есть session_id = sdfadfasdfasdf

Таким образом express-session может взять этот ключ и посмотреть что у него там в объекте есть с таким ключом. Ну ему самому мало это интересно, ему главное в объекте параметра req создать новый объект req.session и туда положить то что у него там есть с таким ключом.

Что такое req.

Это параметр функции контроллера, в котором передается запрос. Если вы посмотрите на этот код:

app.get("/", (req, res) => {
  res.send("Hello world");
});

вот он тут как раз есть, как только Node определил что пользователь запросил маршрут GET / он запускает анонимную функцию, которая передана вторым параметром метода get объекта app. 🙂 надеюсь не сложно. Т.е. есть объект app у него есть метод get, который ждет когда кто-то пришлет запрос GET / и тогда он запустит функцию (req, res) => { код }; Так вот, req — это то что прилетает с браузера, вот то что на картинке во вкладке Request Headers

res — это объект, которому мы говорим что хотим выплюнуть браузеру, например res.send(«text») отправляет какой то текст и закрывает соединение.

Итак, на этом шаге у нас есть минимально рабочий веб-сервер, который отвечает на запрос GET /, ведь все знают что такое GET, POST, PUT и так далее, вот обычный запрос сайта, это GET /

Теперь давайте создадим новый маршрут, секретный, пусть это будет GET /admin, это какая то админка, в которую нам надо попасть только через авторизацию, на самом деле через аутентификацию, но это сложное слово, я не люблю сложные слова, поэтому буду называть тут аутентификацию авторизацией 🙂 в общем создаем новый маршрут:

app.get("/admin", (req, res) => {
  res.send("Admin page");
});

если мы в браузере введем http://localhost:3000/admin то увидим эту страницу, а нам ее надо защитить логином и паролем, чтобы на нее могли попасть только те кто его знают 🙂

Тут начинается самое интересное. Нам нужен еще один маршрут, для авторизации, т.е. страница, которая у нас будет запрашивать логин и пароль. Сделаем ее тоже.

app.get("/login", (req, res) => {
  const html = `
<form method="post" action="/login">
<input type="text" name="username"> <br />
<input type="password" name="password"> <br />
<input type="submit" value="Войти">
</form>
`;
  res.send(html);
});

Наша незамысловатая страница авторизации выглядит следующим образом:

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

Ок, у нас значит заимпортированны passport и его локальная стратегия passport-local

Теперь нам надо познакомиться с таким понятием как middleware в Express.

Это функция use у объекта app, который мы создали из express выше код, кто еще на забыл. И Express вызывает каждую такую функцию при каждом запросе.

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

app.use((req, res, next) => {
  // тут код который выполняется при каждом запросе, и потом надо передать управление дальше, вызвав next();
  console.log('It is middleware');
  next();
});

вот что мы увидим в консоли

server started...
it is middleware

сервер запустился, и при запросе мы увидели it is middleware, т.е. у Express принцип такой, он запускает все функции app.use(…) которые регистрируют какую то функцию, которая ждет три параметра (req, res, next) req — это араметры запроса, res — это объект ответа в браузер, next — это callback запустив который, мы отдаем управление обратно Express’у. И он продолжает делать свои грязные дела 🙂 Итак начинаем кодить, я буду вставлять код в существующий, выделять его жирным и по ходу объяснять что происходит.

const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

app.use(express.urlencoded({extended: false}))

app.get("/", (req, res) => {
  res.send("Hello world");
});

Тут мы добавляем первый middleware, который учит наш Express распознавать формы, прилетающие с браузера. Там чуть повыше мы писали метод GET /login в котором есть форма, вот чтобы мы получили в объекте req данные из этой формы, нам нужно подключить эту middleware.

Дальше мы подключаем middleware для сессии.



app.use(express.urlencoded({extended: false}))

app.use(session({
    secret: "secret",
    resave: false ,
    saveUninitialized: true ,
}))

app.get("/", (req, res) => {
  res.send("Hello world");
});

Это то про что я подробно описывал выше, когда рассказывал про сессии, т.е. мы таким образом внедрили в Express объект, который при каждом запросе смотрит на переменную в Cookie session_id и достает из своих широких штанин, своего объекта session через ключ из Cookie и добавляет в объект req.session={} объект, который у него есть. Мы создаем сессию с параметрами, главный из которых secret, это какая то рандомная строка, для того чтобы наши сессии не могли взломать. Пока об этом рано думать, просто делаем ее какой то уникальной и все. С другими двумя я не разбирался, сказали так делать 🙂 сделали. Это не важно.

app.use(session({
    secret: "secret",
    resave: false ,
    saveUninitialized: true ,
}))

app.use(passport.initialize())
app.use(passport.session())

app.get("/", (req, res) => {
  res.send("Hello world");
});

Добавляем в middleware наш passport, инициализируем его в первой строке, и во второй строке подключаем работу с сессиями. Т.е. таким образом у нас появляется объект passport, который может работать с сессиями, которые мы подключили выше. Насколько я понял, все в конечном итоге складывается в объект req, и из обработчика в обработчик идет через все middleware и в конечном итоге попадает в наш контроллер, где и заканчивает свою жизнь, возвращает в браузер какой то ответ и на этом все.

Дальше нам надо создать функцию, которая будет точкой входа для проверки пользователя для библиотеки passport. Т.е. эта функция будет проверять наши данные, которые прилетели из формы на странице /login , дальше в зависимости от способа проверки лезть например в базу данных, искать там пользователя с таким именем и паролем. И возвращать результат, нашли мы пользователя с такими параметрами или нет. В нашем случае мы все упростим и просто зададим какой то логин и пароль вручную и проверим, правильно ли ввел пользователь логин и пароль и уже в зависимости от этого авторизуем пользователя или нет.

app.use(passport.initialize())
app.use(passport.session())

authUser = (user, password, done) => {
  console.log(`User is ${user}`);
  console.log(`Password is ${password}`);


  if(user=="user1" && password=="password1") {
    return done(null, {id: 1, name: "Andrey"});
  }
    return done (null, false ) 
}

app.get("/", (req, res) => {
  res.send("Hello world");
});

Мы создаем функцию authUser в нее passport передаст 3 параметра user — куда положит имя пользователя из формы на странице /login username, в password положит соответственно пароль и передаст функцию done это callback который нам нужно вызвать, и передать два параметра done(err, user) в первом параметре функция будет ждать ошибку, если null — то ошибки нет. второй параметр, если пользователь не найден, туда передаем false, если пользователь найден, передаем объект с нашим пользователем, там может быть все что угодно, в нашем случае это объект с параметрами id и name. Простейший user 🙂

Тут должно быть понятно, что функция для успешной авторизации ждет от нас user1 логи и пароль password1

authUser = (user, password, done) => {
  ...
}

passport.use(new LocalStrategy (authUser))

app.get("/", (req, res) => {
  res.send("Hello world");
});

Дальше мы уже в middleware passport’а добавляем middleware (т.е. регистрируем плагин в терминологии, которую я выбрал), т.е. регистрируем обработчик или стратегию для passport’а.

Т.е. тут мы регистрируем выбранную нами локальную стратегию и передаем ей нашу функцию authUser, которая будет уже в общем то определять правильный данные ввел пользователь на странице /login или не правильные.

passport.use(new LocalStrategy (authUser))

passport.serializeUser( (user, done) => { 
    done(null, user)
} )


passport.deserializeUser((user, done) => {
        done (null, user )      
}) 

app.get("/", (req, res) => {
  res.send("Hello world");
});

Насколько я понял эти две функции делают следующую работу для passport’а. serializeUser добавляет в сессию passport’а объект с User’ом.

Т.е. смотрите есть req, у него есть объект session, т.е. req.session, когда мы подключали passport’ к сессии, у нас появился объект req.session.passport, и вот функция serializeUser привязывает переданного в параметрах пользователя к этому объекту. Получается req.session.passport.user.{id: 123, name: «Andrey»}

А функция deserializeUser из этой сессии этот объект User’а присваивает уже в req.user = {} в общем как то так это работает он там у себя это делает, а мы в этом месте определяем что нам от него нужно. Ну например мы можем в serialize заставить хранить только ID пользователя, а в deserialize по этому ID уже находить и получать пользователя из БД, чтобы например память не тратить лишний раз, не знаю пока как это работает и для чего нужно. Это есть и работает вот так.

Если кто помнит, в форме авторизации мы определяли <form method=»post» action=»/login»>

Давайте сделаем этот обработчик, который будет ждать из браузера username и password

passport.deserializeUser((user, done) => {
    done(null, user)
})

app.post("/login", passport.authenticate('local', {
    successRedirect: "/admin",
    failureRedirect: "/login",
}));

app.get("/", (req, res) => {
    res.send("Hello world");
});

Здесь когда Express увидит, что прилетел запрос POST /login он его отправит в этот контроллер, куда вторым параметром мы прописываем функцию authenticate из объекта passport которая и будет делать всю грязную работу т.е. вызывать authUser, которая в свою очередь проверит пользователя и вернет false (вторым параметром) или объект User. И тут мы в общем то вторым параметром проверяем пользователь нашелся, значит перенаправляем пользователя на один URL адрес /admin если пользователь ввел не правильные данные, то обратно редиректим на /login чтобы он еще раз ввел логин и пароль.

В общем то это все. Но остался еще один шаг. Нам надо защитить страницу /admin чтобы в нее не попадали без пароля.

app.get("/login", (req, res) => {
    ...
});

auth = (req, res, next) => {
    if (req.user != undefined) next();
    res.redirect("/login");
}

app.get("/admin", auth, (req, res) => {
    res.send("Admin panel");
});


пишем функцию, которая принимает так же 3 параметра (req, res, next) и подсовываем ее в контроллер который хотим защитить, в нашем случае /admin. И прежде чем выполнится функция в admin сначала выполнится функция auth, которая проверит у в объекте req есть ли объект user, который как мы помним создает функция deserializeUser объекта passport. Т.е. если пользователь есть, значит пользователь авторизовался, и мы пропускаем дальше работу запроса через вызов функции next(). Если нет объекта req.user то значит это злоумышленник хочет проникнуть на защищенную страницу без аутентификации ( к концу поста научился писать это слово 🙂 и значит надо отправить его на страницу /login чтобы он ввел логин и пароль, без них мы на страницу /admin его не пропустим.

Вот теперь все, в следующих статьях я уже буду развивать эту тему с авторизацией исходя из первоначальных сведений в этой статье. Т.е. можно попробовать подключиться например к Mongo и использовать ее для хранения пользователей или MySQL, авторизовываться через Google плагин или какой то другой. А также использовать JWT токен. Так как Node все таки будет по большей части работать как микросервис, т.е. маленькая боевая единица, которая делает что-то одно.

Полный код здесь:

const express = require('express');
const app = express();

const session = require("express-session");
const passport = require("passport");
const LocalStrategy = require('passport-local').Strategy;

app.use(express.urlencoded({ extended: false }))
app.use(session({
    secret: "secret",
    resave: false,
    saveUninitialized: true,
}))
app.use(passport.initialize())
app.use(passport.session())
authUser = (user, password, done) => {
    console.log(`User is ${user}`);
    console.log(`Password is ${password}`);


    if (user == "user1" && password == "password1") {
        return done(null, { id: 1, name: "Andrey" });
    }
    return done(null, false)
}
passport.use(new LocalStrategy(authUser))
passport.serializeUser((user, done) => {
    done(null, user)
})
passport.deserializeUser((user, done) => {
    done(null, user)
})

app.post("/login", passport.authenticate('local', {
    successRedirect: "/admin",
    failureRedirect: "/login",
}));

app.get("/", (req, res) => {
    res.send("Hello world");
});

app.get("/login", (req, res) => {
    const html = `
  <form method="post" action="/login">
  <input type="text" name="username"> <br />
  <input type="password" name="password"> <br />
  <input type="submit" value="Войти">
  </form>
  `;
    res.send(html);
});

auth = (req, res, next) => {
    if (req.user != undefined) next();
    res.redirect("/login");
}

app.get("/admin", auth, (req, res) => {
    res.send("Admin panel");
});

app.listen(process.env.PORT || 3000, () => {
    console.log('server started...');
});

Протестирован и работает 😉

Оставьте комментарий