Кроме 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 этот идентификатор 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);
});
Наша незамысловатая страница авторизации выглядит следующим образом:
Ок, у нас значит заимпортированны 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...');
});
Протестирован и работает 😉