Рубрики
Без рубрики

Обработка аутентификации в Express.js

Аутентификация во многих случаях позволяет пользователям персонально выполнять действия на сайте. В этой статье мы будем работать с аутентификацией в Node с помощью Express.js.

Введение в обработку аутентификации в 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/”