Сессии, Куки и Аутентификация

Введение

Понятие "Сессий" основано на том, что состояние пользователя каким-то образом сохраняется, когда он переходит с одной страницы на другую. Вспомните, что HTTP не сохраняет состояний, поэтому только браузер или ваше приложение может "запомнить" то, что нужно запомнить.

В этом уроке вы узнаете о сессиях, куках браузера и о том, как построена аутентификация в Rails. Мы рассмотрим как аутентификацию, создаваемую своими руками, так и наиболее часто используемый для аутентификации гем - Devise.

Пункты для размышления

Постарайтесь ответить на предложенные вопросы. После выполнения задания попробуйте ответить на них ещё раз

  • Что такое сессия?
  • Чем "xэш" сессии отличается от "xэша" куков?
  • Для чего используется "хэш" flash?
  • Когда нужно использовать flash.now вместо flash?
  • Что такое фильтры контроллеров и чем они полезны?
  • Как запустить фильтр контроллера только для нескольких специфических действий?
  • В чем разница между аутентификацией и авторизацией?
  • Чем удобен метод #has_secure_password?
  • Опишите вкратце, как произвести аутентификацию пользователя с помощью этого метода?
  • Какие дополнительные действия (на высоком уровне) нужны, чтобы на самом деле "запомнить" пользователя после того, как он закрыл браузер?
  • Что за гем Devise и чем он полезен?

Куки, Сессии и Флэши

Куки, Сессии и Флэши - это три специальных объекта, которые предоставляет вам Rails 4, и которые ведут себя во многом как хэши. Они используются для хранения данных между запросами. Данные могут сохраняться до следующего запроса, до закрытия браузера или до момента, когда истечет какой-то заранее установленный срок. Кроме времени хранения данных, разница между этими объектами заключается в том, что каждый из них имеет свою определенную область применения, о чем будет рассказно дальше.

Куки

Куки - это пары данных по типу "ключ-значение", которые сохраняются в браузере пользователя до истечения какого-то определенного срока. Они применимы практически для любой задачи, но чаще всего их используют, чтобы сохранить пользователя в том же месте веб-страницы, если он потеряет интернет-соединение, или чтобы хранить простые настройки отображения сайта. Вы можете также хранить в них данные корзины пользователя или даже пароли, но это не очень хорошая идея, не стоит хранить в обычных куках браузера информацию, которая должна быть защищенной или сохраняться между сессиями браузера. Пользователь может легко потерять данные, очистив кэш, или украсть/использовать незащищенные данные из куков.

Для работы с куками Rails дает вам доступ к специальному хэшу под названием cookies, в котором каждая пара ключ-значение сохранена в отдельную куку в браузере пользователя. Если бы вы к примеру сохранили cookies[:hair-color] = "blonde", вы смогли бы открыть инструменты разработчика и увидеть в браузере вашего пользователя куку с ключом hair-color и значением blonde. Удалить их можно, используя cookies.delete(:hair-color).

С каждым новым запросом к серверу браузер отправляет все куки, и вы можете получить к ним доступ в своих котроллерах и представлениях (views) как к обычному хэшу. Вы также можете увидеть их срок действия, к примеру, используя такой синтаксис cookies[:name] = { value: "cookies YUM", expires: Time.now + 3600}.

Сессии

Задумайтесь о том, каким образом браузеры следят, что пользователь залогинен, когда страница перезагружается. HTTP запросы не имеют состояний, так как же вы определите, что запрос пришел именно от залогинненого пользователя? Вот почему важны куки -- они позволяют вам отслеживать пользователя от запроса к запросу, пока не истечет их срок действия.

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

Чтобы идентифицировать сессию юзера, Rails хранит в браузере пользователя специальную безопасную и защищенную от изменений куку, которая содержит весь хэш сессии (поищите ее в инструментах разработчика в браузере, в разделе "Ресурсы"), и она перестает действовать, когда браузер закрывается. Когда бы пользователь ни сделал запрос к вашему приложению, этот запрос автоматически включает куку сессии (как и остальные куки), и вы можете использовать ее, чтобы отслеживать, залогинен ли пользователь. Это все может сейчас казаться абстрактным, но очень скоро вы увидите как это работает на практике.

Rails дает вам доступ к хэшу 'сессии' практически так же, как и к хэшу 'куков'. Используйте переменную session в своих представлениях или контроллерах, вот так:

    # app/controllers/users_controller.rb
    ...
    # Устанавливаем значение сессии
    session[:current_user_id] = user.id

    # Получаем доступ к содержимому сессии
    some_other_variable_value = session[:other_variable_key]

    # Обнуляем ключ сессии
    session[:key_to_be_reset] = nil

    # Обнуляем сессию полностью
    reset_session
    ...

Зачем нужны и куки и сессии? Они похожи, но это не одно и то же. session это весь хэш, который хранится в безопасной куке сессии, который действует до момента закрытия браузера. Если вы посмотрите в инструментах разработчика, то срок действия ("expiration") этой куки обозначен как "сессия". Каждое значение хэша cookies сохраненяется в отдельной куке.

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

Несколько дополнительных замечаний к теме куки и сессии

  • session и cookies это не настоящие хэши, Rails просто притворяется, что это так, чтобы вам было легче работать с ними. Однако вы можете считать их хэшами, потому что они ведут себя очень похоже на настоящие хэши.
  • Размер данных, которые вы можете хранить в хэше сессии или куках браузера ограничен (~4kb). Этого достаточно для любых "обычных" целей, однако не нужно пытаться заменить этим хранилищем базу данных.

Флэши

Вы уже видели и использовали flash хэш, но мы рассмотрим его еще раз, теперь с позиций понимания сессий. flash - это специальный хэш (ну, на самом деле метод, ведущий себя как хэш), который хранит данные только от одного запроса до следующего. Можно представить его как session хэш, который самоуничтожается после открытия. Он обычно используется, чтобы посылать сообщения от контролера к представлению, чтобы пользователь мог видеть сообщения об успешной отправке формы (или о том, что форму не получилось отправить).

Если вы хотите показать сообщение "Спасибо за подписку!" в браузере пользователя, после того, как запустили экшен #create (которое обычно использует redirect_to для отправки пользователя на совершенно новую страницу в случае успеха), как вы отправите это сообщение об успешной подписке? Вы не можете использовать локальную переменную (instance variable), потому что редирект вынуждает браузер выдать совершенно новый HTTP запрос, и все переменные теряются.

Здесь вам на помощь приходит флэш! Используйте для сохранения flash[:success] (можете назвать как угодно) и эти данные будут доступны вашему представлению до следующего запроса. Как только представление откроет хэш, Rails сотрет данные, то есть они не будут показаны при переходе пользователя на новую страницу. Это очень ловко и удобно.

А что если пользователь не смог подписаться, потому что введенные данные не прошли валидацию? В этом случае обычный экшен #create просто отрендерит #new, используя существующие локальные переменные. Это не является новым запросом, и вы, вероятно, захотите сразу же показать пользователю сообщение об ошибке валидации. На этот случай у вас есть удобный хэш flash.now, другими словами flash.now[:error] = "Введите правильные данные!". Как и обычный флэш, это сообщение самоуничтожится после открытия.

Однако вам нужно прописать отображение флэш сообщений в представлениях. Обычно пишется короткий хэлпер, который выведет в верхней части окна браузера любое доступное флэш сообщение(-ия). Также вы можете добавить сообщению класс, который позволит добавить сообщению любые CSS-стили, например выделить :success сообщения зеленым цветом, а :error - красным.

    # app/views/layouts/application.html.erb
    ...
    <% flash.each do |name, message| %>
      <div class="<%= name %>"><%= message %></div>
    <% end %>

Фильтры контроллеров

Прежде, чем мы начнем говорить об аутентификации, необходимо расссмотреть фильтры контроллеров. Суть этих фильтров в том, чтобы запускать некий код в какой-то конкретный момент, к примеру до того, как отработает любой другой код. Это важно, потому что, к примеру если пользователь хочет запустить действие, на которое у него нет прав, вам нужно пресечь этот запрос на корню и вернуть соответствующую ошибку/редирект, до того, как пользователь совершит любое другое действие. Проще говоря, вы "отфильтровываете" неправомерные запросы.

Мы делаем это, используя фильтр "before", который принимает в качестве аргумента имя метода, который мы хотим запустить:

    # app/controllers/users_controller
    before_action :require_login
    ...
    private
    def require_login
      # проверяем, залогинен ли пользователь
    end

Метод before_action принимает символ метода, который будет запущен прежде любого другого кода в контроллере. Если он вернет false или nil, запрос не будет выполнен.

Вы можете назначать фильтр конкретным экшенам с помощью оции only, вот так: before_action :require_login, only: [:edit, :update]. Или наоборот, если вы используете опцию :except, фильтр сработает для всех экшенов кроме указанного.

Лучше прятать фильтрующие методы в 'private', так чтобы они были доступны только из своего контроллера.

Ну и последнее, фильтры могут наследоваться, так что если вы хотите применить фильтр к экшенам абсолютно всех контроллеров, поместите его в файл app/controllers/application_controller.rb.

Аутентификация

Основная цель аутентификации - это удостовериться в том, что тот, кто пытается выполнить какое-либо действие, имеет на это право. Стандартный способ управления этим процессом заключается в том, что пользователь логинится через форму входа. Как только пользователь залогинен, вы отслеживаете его дальнейшие действия с помощью сессий, пока он не выйдет.

Авторизация - это другая концепция, связанная с аутентификацией. Вы можете быть залогинены, но есть ли у вас права на действия, которые вы хотите осущестивить (просмотреть страницу, изменить или удалить элемент)? Банальный пример это разница между обычным пользователем и администратором сайта. Они оба залогинены, но только у админа есть права на изменение определенных вещей.

Аутентификация и авторизация идут рука об руку -- сперва вы аутентифицируете кого-то, чтобы понять, кто это, и далее можете проверить, есть ли у них права на просмотр страницы или выполнение действия. При создании приложения у вас будет система аутентификации, чтобы пользователь мог залогиниться и подтвердить, что это именно он. Вы разрешаете пользователю делать определенные вещи (например, удалять какой-то контент), основываясь на том, какие методы защищены фильтрами контроллера, требующими входа на сайт или наличия определенных прав (например, прав администратора).

Аутентификация Basic и Digest

Если вам нужен простой и небезопасный способ аутентификации, то можно использовать аутентификацию HTTP Basic. Мы не будем здесь рассматривать ее подробно, но в общем она подразумевает ввод имени пользователя и пароля в простую форму и отправку их (в незашифрованном виде) через сеть. Для этого используется метод #http_basic_authenticate_with (примеры смотрите в разделе дополнительные ресурсы), и этот же метод ограничивает доступ к определенным контроллерам для неаутентифицированных пользователей.

Для чуть более безопасной (через HTTP) системы аутентификации, используйте аутентификацию HTTP Digest. Опять же, мы не будем ее здесь подробно расписывать. Она основана на запуске метода через #before_action и дальнейшем вызове #authenticate_or_request_with_http_digest, который принимает блок, который должен вернуть "верный" пароль, который должен быть предоставлен.

Проблема с этими способами заключается в том, что имена и пароли в явном виде находятся в контроллере (или где-то еще), так что это можно использовать только как временное решение.

Создаем свою аутентификацию

Если вы хотите, чтобы пользователи могли залогиниться на сайте, придется выполнить еще несколько действий. Мы не будет расписывать их подробно, потому что вы сможете все это проделать на практике. Однако некоторые принципы знать полезно. То, о чем мы будем говорить дальше, может показаться немного непонятным и отвлеченным от реальных примеров, однако это на самом деле всего лишь краткое описание тех штук, которые вы скоро будете внедрять в своем проекте.

Во-первых, мы не храним пароли в базе данных в виде текста. Делая так, мы просто напрашиваемся на неприятности (спорим, вы тоже читали новости о взломанных сайтах и утекших паролях?). Вместо этого, мы храним зашифрованную версию пароля ("password digest").

Когда пользователь вводит свой пароль в форму входа, вместо того, чтобы сравнить ее с текстом пароля напрямую, вы шифруете пароль. Затем вы сравниваете хэшированную версию пароля с хешированным паролем, хранящимся в базе. Если они совпадают, у вас появляется залогиненный пользователь.

Это гораздо лучше, потому что, если вы помните то, о чем мы говорили в уроке по безопасности в веб-разработке 101, хэши - это односторонняя функция. Вы можете легко хэшировать текст пароля, однако очень и очень трудно расшифровать его обратно и восстановить исходный пароль. Самый эффективный способ взломать набор хэшей - это собрать огромный лист возможных паролей, хешировать их и сравнить с теми, которые вы пытаетесь взломать (т.е. это массированный подбор паролей).

Rails не заставляет вас делать все самостоятельно. Вам предоставляется метод под названием #has_secure_password, который вы просто встраиваете в модель User и он добавляет множество возможностей, которые вам нужны. Чтобы работать с этим удобным методом, вы настраивате в модели User обработку атрибутов password и password_confirmation, однако не сохраняете эти значения в базе данных. Метод has_secure_password взаимодействует с их значениями, превращая их в хэши.

Чтобы инициализировать новую сессию пользователя (когда он логинится), вам нужно создать новый контроллер (обычно это sessions_controller.rb) и соответствующие роуты для :new, :create и :destroy. Если пользователь передает корректные логин и пароль (а мы проверяем это, используя метод #authenticate), вы сохраняете ID пользователя в переменную session, которую можно использовать для подтверждения того, что пользователь на самом деле тот, кем представился. Это простой способ аутентификации пользователя, который использует существующую инфраструктуру сессий Rails, но данные в нем хранятся только пока существует сессия.

Если ваш пользователь хочет, что его "запомнили" (вы, наверное, тысячу раз видели чекбокс "запомнить меня" в форме входа на сайт), вам нужен способ запомнить его на более долгое время, чем длится сессия браузера. Чтобы это сделать, вам нужно будет создать еще один столбец в таблице Users, где будет храниться зашифрованный remember_token (вообще вы можете назвать его как угодно). Вы будете использовать его, чтобы сохранить рандомную строку для этого пользователя, которая будет в дальнейшем использована для его идентификации.

Вы будете отдавать незашифрованный токен в браузер пользователя в качестве постоянной куки (используя cookies.permanent[:remember_token]). Эта кука будет отправляться с каждым новым запросом, так что вы сможете сверить ее с зашифрованным образцом в базе данных, чтобы проверить, кем является пользователь, когда он сделает запрос. Это более явная и длительная версия того, что Rails делает с сессиями. Принято делать сброс токена каждый раз, когда пользователь выходит с сайта и логинится заново.

Обычно полезно иметь несколько методов-хелперов для решения обычных задач: залогинить пользователя, проверить, залогинен ли он, или сравнить залогиненного пользователя с другим пользователем (это пригождается, когда текущий пользователь смотрит страницу другого пользователя, и не должен видеть ссылки на "редактирование"). Эти штуки сделают вашу жизнь легче, потому что вы сможете повторно использовать их в фильтрах контроллерах, вьюхах или даже в своих тестах.

Общее пошаговое описание:

  1. Добавьте в таблицу Users столбец, который будет содержать password_digest пользователя.
  2. Когда пользователь зарегистрируется, хэшируйте его пароль и сохраните ХЭШИРОВАННУЮ версию в новом столбце базы данных, с помощью метода has_secure_password, добавленного в модель User.
  3. Не забывайте о необходимой валидации длины пароля и подтверждения пароля.
  4. Создайте контроллер для сесссий (и соответствующих роутов) и используйте метод #authenticate, чтобы пользователь мог залогиниться после того как ввел верные данные в форму входа.
  5. Запоминайте пользователей, создав столбец remember_token в таблице Users и сохраняя этот токен как постоянную куку в браузере пользователя. Обнуляйте ее при каждом новом входе на сайт.
  6. При каждой загрузке страницы, которая требует аутентификации (и использования #before_action в соответствующем контроллере) первым делом проверьте сравните куку remember_token с базой данных, чтобы понять, залогинен ли уже пользователь. Если нет, перенаправьте его на страницу входа.
  7. По необходимости создайте методы-хелперы, которые позволят вам делать вещи вроде простого определения, залогинен ли пользователь, или сравнить текущего пользователя с другим.
  8. Профит.

Devise

Devise - это гем, который был создан, чтобы делать за вас все вышеописанное. Вам может захотеться немедленно заняться его изучением, но это не очень хорошая идея для новичков. Сначала крайне необходимо понять базовые аспекты аутентификации. Devise также может оказаться для вас весьма сложным, если вы столкнетесь в проблемами, которые не имеют стандартных решений. В общем, это гем более полезен для Rails разработчиков среднего уровня, чем для новичков.

Несмотря на это, в итоге вы будете использовать его в большинстве своих проектов, после того как освоите самостоятельно создание аутентификации. Использовать Devise несравнимо лучше, чем строить аутентификацию своими руками, потому что он решает множество проблем и закрывает потенциальные лазейки в безопасности, о которых вы даже бы и не подумали. Devise позволяет вам взаимодействовать с более продвинутыми системами аутентификации при работе с API, такими как OAuth. Поэтому, чуть позже он будет очень для вас полезен.

Вкратце, Devise предоставляет вам набор форм для регистрации и входа на сайт, а также методы для их внедрения. Он состоит из 10 модулей (и вы можете выбрать, какие именно будете использовать). Вы устанавливаете гем 'devise' и запускаете установщик, чтобы перенести файлы в свое приложение. Вам также нужно будет запустить миграцию базы данных, чтобы добавить дополнительные поля в свою таблицу Users.

Конфигурация будет зависеть от ваших предпочтений. Хотите ли вы, чтобы пользователи подтверждали регистрацию по e-mail (модуль Confirmable)? Хотите ли позволить пользователю сбросить пароль (модуль Recoverable)?

Изучение гема Devise выходит за рамки этого урока, но вы совершенно точно будете использовать его к концу курса. Просто читайте документацию. Документация Devise впечатляет, она доступна на Github. Мы приводим ссылку, чтобы вы взглянули на нее, почитали, и сохранили где-то в подкорке до той поры, когда начнете использовать этот гем.

Ваши задания

  1. Прочтите Rails Guides on Controllers главы 5-6. Сейчас не слишком заморачивайтесь с деталями конфигураций session_store в п.5.1.
  2. Прочтите Rails Guides on Controllers главу 8, чтобы понять фильтры контроллеров.
  3. Прочтите Rails guides on Controllers главу 11, чтобы больше понять об аутентификации.
  4. Бегло просмотрите Devise Documentation. Прочтите о том, как установить этот гем в ваше Rails-приложение, и за что отвечают разные модули. Вам не нужно использовать Devise прямо сейчас, так что это просто разведка на будущее.

Заключение

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

Этот урок должен был дать вам представление о том, насколько сложными могут быть системы аутентификации, однако он должен был также убрать налет таинственности с механизмов работы сайтов, на которых вы бывали сотни раз. Аутентификация - не ракетостроение, все гораздо проще, и уже скоро вы сами будете встраивать ее в свое приложение.

Дополнительные ресурсы

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

  • Аутентификация в Rails 3.1 из Railscasts... лучше, чем я сам могу объяснить.
  • Куки и Сессии в Rails 4 от pothibo.com
  • Все о куках

Поделиться уроком: