Простой сайт с возможностью авторизации на node.js

В этой статье я попробую рассказать о том, как с помощью node.js и connect сделать простой сайт с авторизацией. Т.е. такой, где часть контента доступна всем, а часть — только зарегистрированным пользователям. Поскольку express.js основан на connect, практически все, что здесь написано, относится и к нему тоже.
Допустим, что вы уже знаете, что такое node.js и как с ним работать. Также допустим, что у вас уже есть простенький сайт с основной страницей и парой дополнительных. Вот здесь — исходники такого сайта, пример для этой статьи.

Теория

Есть такое понятие, как сессия — период времени, пока пользователь находится на сайте. Сессия начинается, когда пользователь впервые открывает сайт в браузере и заканчивается, когда у нее истечет срок действия (или когда сайт захочет ее прервать). С каждой сессией связывается определенный набор данных:
  • уникальный идентификатор сессии
  • срок действия
  • имя пользователя (если он его введет)
  • другая служебная информация, необходимая для идентификации: ip-адрес, user agent
  • любая другая информация, которую сайт связывает с текущим пользователем. Например, электронный магазин может хранить в сессии список товаров, добавленных в корзину.

Для решения нашей задачи нужны две таблицы в базе данных: одна для хранения данных сессии, другая — для информации о пользователях. На самом деле, в данном случае говорить «таблица БД» не совсем правильно, информация может находиться в разных местах. Например, всю параметры сессии можно хранить в cookies (или в памяти приложения, хотя это и нехорошо). Данные о пользователе могут поступать извне, если он заходит с помощью OpenID/OAuth.

Connect

Всю работу по созданию сессии connect берет на себя. Для этого нужно добавить два правила:
app.use(connect.cookieParser());
app.use(connect.session({ secret: 'your secret here'} ));

Порядок имеет значение, сами правила должны быть определены до задания маршрутов. Первое правило обеспечивает работу с куками в общем. Второе добавляет к обычному request поле session, через которое будут доступны данные сессии (дальше с примерами станет понятнее).

connect.session получает такие параметры:
  • secret — фраза, которая используется для шифрования информации в cookies.
  • store — обьект, который будет использоваться для хранения данных сессии. По умолчанию connect хранит все данные в памяти, но, естественно, в реальных приложениях так делать нельзя. Есть готовые решения для mongodb, redis, MySQL и т.д.
  • cookie — набор параметров cookie. Самый важный — maxAge, время жизни в миллисекундах (или null)

Авторизация

Как уже было сказано, connect будет добавлять поле session к каждому запросу, но по умолчанию там ничего интересного нет. Если мы каким-то образом «узнаем» пользователя (собственно, если он введет правильный пароль), мы должны будем сами добавить информацию о нем к сессии. Приблизительно так:
if ((request.body.login==='Thor')&&(request.body.password==='111')) {
    request.session.authorized = true;
    request.session.username = request.body.login;

    console.log('Thor is here!');
}

В принципе, хватило бы одной переменной username (так делает автор вот этой статьи). Но тогда проверка, авторизирован ли пользователь, будет выглядеть некрасиво:
if (typeof req.session.username == 'undefined') {
 // не залогинен,  перенаправить на форму ввода пароля
}

Когда пользователь захочет разлогиниться, достаточно будет просто удалить добавленные поля:
  delete req.session.authorized;
  delete req.session.username ;

Для полной очистки есть метод session.destroy(). Он удаляет session из текущего запроса, а при следующем это поле будет сгенерировано заново.

Контроль доступа

Наиболее очевидное решение — проверять request.session.authorized всякий раз, когда нужно сгенерировать защищенную страницу. Собственно, так и делают в статье, на которую я уже ссылался. Проблема в том, это противоречит «слоистой» идеологии connect. Лучше задать специальное правило, которое будет проверять права пользователя и, если что не так, перенаправлять его на страницу ошибки. Идея описана здесь, в нашем случае будет
// адреса, которые поддерживает наш сайт; 
var siteUrls = [
  {pattern:'^/login/?$', restricted: false}
, {pattern:'^/logout/?$', restricted: true}
, {pattern:'^/$', restricted: false}
, {pattern:'^/single/\\w+/?$', restricted: true}
];

function authorizeUrls(urls) {
  function authorize(req, res, next) {
    var requestedUrl = url.parse(req.url).pathname;
    for (var ui in urls) {
      var pattern = urls[ui].pattern;
      var restricted = urls[ui].restricted;
      if (requestedUrl.match(pattern)) {
        if (restricted) {
          if (req.session.authorized) {
            // если все хорошо, просто переходим к следующим правилам
            next();
            return;
          }
          else{
            // пользователь не авторизирован, отправляем его на страницу логина
            res.writeHead(303, {'Location': '/login'});
            res.end();
            return;
          }
        }
        else {
          next();
          return;
        }
      }
    }

    // сюда мы попадаем, только если в цикле не нашлось совпадений
    console.log('common 404 for ', req.url);
    res.end('404: there is no ' + req.url + ' here');
  }
  return authorize ;
}

app.use('/', authorizeUrls(siteUrls));


Все. Надеюсь, это кому-нибудь поможет.

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 8 965 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 15

    +3
    nodeguide.ru/doc/dailyjs-nodepad/node-tutorial-5/#id3

    В данной статье имеется хорошая реализация контроля доступа, более правильного, на мой взгляд.
      0
      «Но тогда проверка, авторизирован ли пользователь, будет выглядеть некрасиво...»
      В js есть «красивое» приведение к boolean с помощью двух восклицательных знаков:
      if(!!req.session.username) {}
        +4
        В js всё, что находится внутри if, автоматом приводится к boolean. Как, собственно, и в других реализациях ecma.
        +3
        if (requestedUrl.match(pattern)) ...
        

        Если не используете результат, то лучше «test»

        if (pattern.test(requestedUrl)) ...
        
          0
          А чем лучше? Всеравно кусок «найдется» просто не возвращается во втором случае.
            0
            Вы, можно сказать, сами ответили на свой вопрос.
            test — функция, по логике своей, возвращающая бинарное значение.
            if — условный оператор использующий на вход тоже самое.
            Вам не кажется, что они отлично подходят друг другу? :-)

            Конкретно в этом случае разница по скорости ничтожна, но в целом считаю test более уместным для таких вещей. В любом случае отработает не медленней, а то и быстрее в зависимости от регулярки и количества возвращенных данных.

              0
              Просто если лишь в этом дело, тогда ладно. Разница еще в том что test запоминает позицию, и это свойство может повлечь за собой в дальнейшем ошибки, поэтому я всегда match использую =) Просто думал что может какая-то серьезная разница.
          0
          Если цель статьи просто показать как проверять авторизован ли пользователь или нет используя «прослойки», то имхо сильно заумно получилось.

          Ну и само написание кода вызывает нарекания.
            0
            Как вариант. Но я использую другой подход, который даёт больше гибкости — проверка на уровне прослоек маршрутов:
            var //...
                middleWare_isLogged = [auth.checkUser],
                middleWare_canEditUsers = [auth.checkUser, auth.hasPrivilege('editUsers')];
            
            app.get('/', function (req, res) {
            // просто отдаём страницу
            
            // some code here
            });
            
            app.get('/users', middleWare_isLogged, function (req, res) {
            // проверям, залогинен ли пользователь
            
            // some code here
            });
            
            app.post('/users/edit', middleWare_canEditUsers, function (req, res) {
            // проверяем, имеет ли залогиненый пользователь привилегию "editUsers"
            
            // some code here
            });
            
              0
              подскажите пожалуйста, что такое auth?
                0
                Отдельный рукописный модуль — auth.js, в котором как раз и описаны функции проверки.
                Могу описать подробнее, если надо.
              • НЛО прилетело и опубликовало эту надпись здесь
                  +1
                          hasPrivilege: function (priv) {
                              return function (req, res, next) {
                  
                  // тут всяко-разные проверки - по свойствм req.session смотрим, имеет ли данный 
                  // пользователь привилегию priv и если да - result=true.
                  // ну а далее либо просто передаём управление дальше если всё ок, 
                  // либо передаём ошибку "недостаточно прав"
                  
                                  if (result) {
                                      next(); 
                                  }
                                  else {
                                      next({ msg: const.ERR_AUTH_PERMISSION_DENIED, status: 403 });
                                  }
                              };
                          }
                  
                0
                Посмотрите в сторону passport
                • НЛО прилетело и опубликовало эту надпись здесь

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

                Самое читаемое