Введение в обработку аутентификации в Express.js
В этой статье мы создадим простое приложение, чтобы продемонстрировать, как можно управлять аутентификацией в Express.js. Поскольку мы будем использовать некоторые базовые синтаксисы ES6 и фреймворк Bootstrap для дизайна пользовательского интерфейса, возможно, вам пригодятся базовые знания об этих технологиях.
Несмотря на то, что в реальном приложении вам может понадобиться использовать базу данных, поскольку нам нужно сохранить простоту этой статьи, мы не будем использовать базы данных или методы проверки электронной почты, такие как отправка письма с кодом проверки.
Настройка проекта
Для начала создадим новую папку с названием, скажем, simple-web-app. Используя терминал, перейдем в эту папку и создадим скелетный проект Node.js:
$ npm init
Теперь мы можем установить и Express:
$ npm install --save express
Чтобы упростить ситуацию, мы будем использовать серверный механизм рендеринга под названием Handlebars. Этот движок будет рендерить наши HTML-страницы на стороне сервера, благодаря чему нам не понадобится никакой другой frontend-фреймворк, такой как Angular или React.
Давайте продолжим и установим express-handlebars:
$ npm install --save express-handlebars
Мы также будем использовать два других промежуточных пакета Express (body-parser и cookie-parser) для разбора тел HTTP-запросов и разбора куки, необходимых для аутентификации:
$ npm install --save body-parser cookie-parser
Реализация
Приложение, которое мы собираемся создать, будет содержать “защищенную” страницу, которую смогут посещать только зарегистрированные пользователи, в противном случае они будут перенаправлены на главную страницу, где им будет предложено либо войти, либо зарегистрироваться.
Чтобы начать работу, давайте импортируем библиотеки, которые мы ранее установили:
const express = require('express'); const exphbs = require('express-handlebars'); const cookieParser = require('cookie-parser'); const bodyParser = require('body-parser');
Мы будем использовать встроенный модуль Node crypto для хеширования паролей и генерации токена аутентификации – об этом будет рассказано немного позже в статье.
Далее создадим простое приложение Express и настроим импортированное нами промежуточное ПО вместе с движком Handlebars:
const app = express(); // To support URL-encoded bodies app.use(bodyParser.urlencoded({ extended: true })); // To parse cookies from the HTTP Request app.use(cookieParser()); app.engine('hbs', exphbs({ extname: '.hbs' })); app.set('view engine', 'hbs'); // Our requests hadlers will be implemented here... app.listen(3000);
По умолчанию в Handlebars расширение шаблона должно быть .handlebars. Как вы можете видеть в этом коде, мы настроили наш шаблонизатор handlebars на поддержку файлов с более коротким расширением .hbs. Теперь давайте создадим несколько файлов шаблонов:
В папке layouts внутри папки view будет храниться ваш основной макет, который обеспечит базовый HTML для других шаблонов.
Давайте создадим main.hbs, нашу главную страницу-обертку:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> </head> <body> <div class="container"> {{{body}}} </div> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> </body> </html>
Другие шаблоны будут отображаться внутри тега {{body}} этого шаблона. У нас есть HTML-шаблон и необходимые CSS и JS файлы для Bootstrap, импортированные в этот макет.
Когда наша основная обертка готова, давайте создадим страницу home.hbs, на которой пользователям будет предложено войти или зарегистрироваться:
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="#">Simple Authentication App</a> </nav> <div style="margin-top: 30px"> <a class="btn btn-primary btn-lg active" href="/login">Login</a> <a class="btn btn-primary btn-lg active" href="/register">Register</a> </div>
Затем создадим обработчик запроса к корневому пути (/) для рендеринга шаблона home.
app.get('/', function (req, res) { res.render('home'); });
Давайте запустим наше приложение и перейдем на сайт http://localhost:3000:
Регистрация аккаунта
Информация об аккаунте собирается через страницу registration.hbs:
<div class="row justify-content-md-center" style="margin-top: 30px"> <div class="col-md-4"> {{#if message}} <div class="alert {{messageClass}}" role="alert"> {{message}} </div> {{/if}} <form method="POST" action="/register"> <div class="form-group"> <label for="firstNameInput">First Name</label> <input name="firstName" type="text" class="form-control" id="firstNameInput"> </div> <div class="form-group"> <label for="lastNameInput">Last Name</label> <input name="firstName" type="text" class="form-control" id="lastNameInput"> </div> <div class="form-group"> <label for="emailInput">Email address</label> <input name="email" type="email" class="form-control" id="emailInput" placeholder="Enter email"> </div> <div class="form-group"> <label for="passwordInput">Password</label> <input name="password" type="password" class="form-control" id="passwordInput" placeholder="Password"> </div> <div class="form-group"> <label for="confirmPasswordInput">Confirm Password</label> <input name="confirmPassword" type="password" class="form-control" id="confirmPasswordInput" placeholder="Re-enter your password here"> </div> <button type="submit" class="btn btn-primary">Login</button> </form> </div> </div>
В этом шаблоне мы создали форму с полями регистрации пользователя, а именно: имя, фамилия, адрес электронной почты, пароль и подтверждение пароля, и задали наше действие как маршрут /register. Также у нас есть поле сообщения, в котором мы будем отображать сообщения об ошибке и успехе, например, если пароли не совпадают и т.д.
Давайте создадим обработчик запроса для отображения шаблона регистрации, когда пользователь посетит сайт http://localhost:3000/register:
app.get('/register', (req, res) => { res.render('register'); });
По соображениям безопасности, хорошей практикой является хэширование пароля с помощью надежного алгоритма хэширования, например SHA256. Хешируя пароли, мы гарантируем, что даже если наша база данных паролей может быть взломана, пароли не будут просто лежать на виду в текстовом формате.
Еще более эффективным методом, чем простое хеширование, является использование соли, как в алгоритме bcrypt. Более подробную информацию о защите аутентификации можно найти в статье Реализация аутентификации пользователей правильным способом. Однако в этой статье мы будем действовать немного проще.
const crypto = require('crypto'); const getHashedPassword = (password) => { const sha256 = crypto.createHash('sha256'); const hash = sha256.update(password).digest('base64'); return hash; }
Когда пользователь отправит форму регистрации, по пути /register будет отправлен POST-запрос.
Учитывая это, нам теперь нужно обработать этот запрос с информацией из формы и сохранить нашего вновь созданного пользователя. Обычно это делается путем сохранения пользователя в базе данных, но для простоты мы будем хранить пользователей в массиве JavaScript.
Поскольку при каждом перезапуске сервера массив будет инициализироваться заново, для тестирования мы жестко закодируем пользователя, который будет инициализироваться каждый раз:
const users = [ // This user is added to the array to avoid creating a new user on each restart { firstName: 'John', lastName: 'Doe', email: 'johndoe@email.com', // This is the SHA256 hash for value of `password` password: 'XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=' } ]; app.post('/register', (req, res) => { const { email, firstName, lastName, password, confirmPassword } = req.body; // Check if the password and confirm password fields match if (password === confirmPassword) { // Check if user with the same email is also registered if (users.find(user => user.email === email)) { res.render('register', { message: 'User already registered.', messageClass: 'alert-danger' }); return; } const hashedPassword = getHashedPassword(password); // Store user into the database if you are using one users.push({ firstName, lastName, email, password: hashedPassword }); res.render('login', { message: 'Registration Complete. Please login to continue.', messageClass: 'alert-success' }); } else { res.render('register', { message: 'Password does not match.', messageClass: 'alert-danger' }); } });
Полученные email, FirstName, LastName, passtord и confirmPassword проверяются – совпадают ли пароли, не зарегистрирован ли уже email и т.д.
Если каждая проверка прошла успешно, мы хэшируем пароль, сохраняем информацию в массиве и перенаправляем пользователя на страницу регистрации. В противном случае мы повторно отобразим страницу регистрации с сообщением об ошибке.
Теперь давайте посетим конечную точку /register, чтобы убедиться, что она работает правильно:
Вход в аккаунт
С регистрацией покончено, теперь мы можем реализовать функциональность входа в систему. Начнем с создания страницы login.hbs:
<div class="row justify-content-md-center" style="margin-top: 100px"> <div class="col-md-6"> {{#if message}} <div class="alert {{messageClass}}" role="alert"> {{message}} </div> {{/if}} <form method="POST" action="/login"> <div class="form-group"> <label for="exampleInputEmail1">Email address</label> <input name="email" type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email"> </div> <div class="form-group"> <label for="exampleInputPassword1">Password</label> <input name="password" type="password" class="form-control" id="exampleInputPassword1" placeholder="Password"> </div> <button type="submit" class="btn btn-primary">Login</button> </form> </div> </div>
А затем создадим обработчик и для этого запроса:
app.get('/login', (req, res) => { res.render('login'); });
Эта форма будет отправлять POST-запрос на /login, когда пользователь отправит форму. Еще одна вещь, которую мы будем делать, это отправка маркера аутентификации для входа в систему. Этот маркер будет использоваться для идентификации пользователя, и каждый раз, когда он будет отправлять HTTP-запрос, этот маркер будет отправляться в виде cookie:
const generateAuthToken = () => { return crypto.randomBytes(30).toString('hex'); }
С помощью нашего вспомогательного метода мы можем создать обработчик запроса для страницы входа в систему:
// This will hold the users and authToken related to users const authTokens = {}; app.post('/login', (req, res) => { const { email, password } = req.body; const hashedPassword = getHashedPassword(password); const user = users.find(u => { return u.email === email && hashedPassword === u.password }); if (user) { const authToken = generateAuthToken(); // Store authentication token authTokens[authToken] = user; // Setting the auth token in cookies res.cookie('AuthToken', authToken); // Redirect user to the protected page res.redirect('/protected'); } else { res.render('login', { message: 'Invalid username or password', messageClass: 'alert-danger' }); } });
В этом обработчике запроса используется map authTokens для хранения токенов аутентификации в качестве ключа и соответствующего пользователя в качестве значения, что позволяет осуществлять простой поиск токена по пользователю. Вы можете использовать базу данных, например Redis, или вообще любую базу данных для хранения этих токенов – мы используем эту карту для простоты.
Нажав на конечную точку /login, мы получим следующее приветствие:
Однако мы еще не закончили. Нам нужно будет внедрить пользователя в запрос, считав authToken из cookies при получении запроса на вход. Над всеми обработчиками запросов и под промежуточным ПО парсера куки, давайте создадим наше собственное промежуточное ПО для внедрения пользователей в запросы:
app.use((req, res, next) => { // Get auth token from the cookies const authToken = req.cookies['AuthToken']; // Inject the user to the request req.user = authTokens[authToken]; next(); });
Теперь мы можем использовать req.user внутри наших обработчиков запросов, чтобы проверить, аутентифицирован ли пользователь с помощью токена.
Наконец, давайте создадим обработчик запроса для рендеринга защищенной страницы – protected.hbs:
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="#">Protected Page</a> </nav> <div> <h2>This page is only visible to logged in users</h2> </div>
И обработчик запроса для страницы:
app.get('/protected', (req, res) => { if (req.user) { res.render('protected'); } else { res.render('login', { message: 'Please login to continue', messageClass: 'alert-danger' }); } })
Как видите, вы можете использовать req.user, чтобы проверить, аутентифицирован ли пользователь. Если этот объект пуст, пользователь не аутентифицирован.
Другой способ потребовать аутентификацию на маршрутах – реализовать ее как промежуточное программное обеспечение, которое затем можно применять к маршрутам непосредственно, когда они определены с помощью объекта app:
const requireAuth = (req, res, next) => { if (req.user) { next(); } else { res.render('login', { message: 'Please login to continue', messageClass: 'alert-danger' }); } }; app.get('/protected', requireAuth, (req, res) => { res.render('protected'); });
Стратегии авторизации также могут быть реализованы таким образом путем назначения ролей пользователям и последующей проверки правильности разрешений до того, как пользователь получит доступ к странице.
Заключение
Аутентификация пользователей в Express довольно проста и понятна. Мы использовали встроенный модуль Node crypto для хэширования паролей зарегистрированных пользователей в качестве базовой функции безопасности и создали защищенную страницу, видимую только пользователям, прошедшим аутентификацию с помощью маркера.
Исходный код этого проекта можно найти на GitHub.
Оригинал: “https://stackabuse.com/handling-authentication-in-express-js/”