Опубликован: 27.01.2016 | Доступ: свободный | Студентов: 914 / 58 | Длительность: 23:07:00
Лекция 8:

Войти, выйти

Успешный вход

Получив обработку неудачного входа, теперь нам нужно на самом деле впустить пользователя. Получение этого результата потребует самого сложного Ruby программирования, которое мы когда либо встречали в этом учебнике, так что держитесь до конца и будьте готовы к небольшому количеству тяжелой работы. К счастью, первый шаг прост — завершение create действия контроллера Sessions — простая задача. К сожалению, эта легкость обманчива.

Заполнить область, занятую в настоящее время комментарием (Листинг 8.12) легко: после успешного входа, мы впускаем пользователя, используя функцию sign_in, а затем перенаправляем его на страницу профиля (Листинг 8.13). Мы видим теперь, почему это обманчивая легкость: увы, sign_in в настоящее время не существует. Написание этой функции займет оставшуюся часть этого раздела.

class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_to user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end
Листинг 8.13. Завершенное действие create контроллера Sessions (пока не рабочее). app/controllers/sessions_controller.rb

Запомнить меня

Мы теперь в состоянии приступить к реализации нашей модели входа, а именно, запоминанию статуса вошедшего пользователя "навсегда" и очистке сессии только тогда, когда пользователь явно покинет наш сайт. Сами функции входа, в конечном итоге, пересекают традиционное Модель-Представление-Контроллер; в частности, несколько функций входа должны быть доступны и в контроллерах и в представлениях. Вы можете вспомнить из Раздела 4.2.5, что Ruby предоставляет модули для упаковки функций вместе и включения их в нескольких местах и это наш план для функций аутентификации. Мы могли бы сделать совершенно новый модуль для аутентификации, но контроллер Sessions уже оснащен модулем, а именно, SessionsHelper. Кроме того, помощники автоматически включаются в Rails представления, так что все что мы должны сделать для того чтобы использовать функции Sessions хелпера в контроллерах, это включить соответствующий модуль в Application контроллер (Листинг 8.14).

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end
Листинг 8.14. Включение модуля SessionsHelper в контроллер Application. app/controllers/application_controller.rb

По умолчанию, все помощники доступны во views, но не в контроллерах. Нам нужны методы Sessions хелпера в обоих местах, поэтому мы должны явно включить его.

Поскольку HTTP является протоколом, не сохраняющим своего состояния, веб-приложения, требующие входа пользователей, должны реализовывать способ, позволяющий отслеживать прогресс каждого пользователя от страницы к странице. Один из методов для поддержания статуса вошедшего пользователя, является использование традиционных Rails сессий (с помощью специальной session функции) для хранения remember token, равного пользовательскому id:

session[:remember_token] = user.id

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

User.find(session[:remember_token])

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

Для выбранного нами способа, который подразумевает постоянные сессии — то есть статус вошедшего пользователя, сохраняющийся даже после того, как браузер закрыт — нам необходимо использовать постоянный идентификатор для вошедшего пользователя. Для того чтобы достигнуть этого, мы будем генерировать уникальный, безопасный remember token для каждого пользователя и мы будем хранить его в качестве постоянной куки отличающейся от обычной тем, что она не истекает при закрытии браузера.

Remember token должен быть связан с пользователем и должен сохраняться для последующего использования, поэтому мы добавим его в качестве атрибута модели User, как это показано на рис. 8.8 .

Модель User с добавленным атрибутом remember_token.

Рис. 8.8. Модель User с добавленным атрибутом remember_token.

Мы начнем с небольшого дополнения к спекам модели User (Листинг 8.15).

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:password_confirmation) }
  it { should respond_to(:remember_token) }
  it { should respond_to(:authenticate) }
  .
  .
  .
end
Листинг 8.15. Первый тест для remember token. spec/models/user_spec.rb

Мы можем получить прохождение этого теста сгенерировав remember token в командной строке:

$ rails generate migration add_remember_token_to_users

Затем мы заполняем получившуюся миграциюю кодом из Листинга 8.16. Это дает нам код показанный в Листинге 8.16. Обратите внимание, что, посколку мы планируем искать пользователей в базе данных по remember token, мы должны добавить индекс (Блок 6.2) к столбцу remember_token

class AddRememberTokenToUsers < ActiveRecord::Migration
  def change
    add_column :users, :remember_token, :string
    add_index  :users, :remember_token
  end
end
Листинг 8.16. Миграция для добавления remember_token к таблице users. db/migrate/[ts]_add_remember_token_to_users.rb

Затем мы, как обычно, обновляем тестовую и рабочую базы данных:

$ bundle exec rake db:migrate
$ bundle exec rake test:prepare

В этой точке спеки модели User должны проходить:

$ bundle exec rspec spec/models/user_spec.rb

Теперь мы должны выбрать, что именно использовать в качестве remember token. Существует множество, в основном эквивалентных способов, по сути, подойдет любая длинная случайная строка если она будет уникальной. Метод urlsafe_base64 из модуля SecureRandom стандартной библиотеки Ruby вполне соответствует нашим требованиям:2Этот выбор опирается на RailsCast on remember me. он возвращает случайную строку длиной в 16 символов составленную из знаков A–Z, a–z, 0–9, "-" и "_" (в общей сложности 64 возможности, т.е. "base64"). Это означает, что вероятность того, что два remember токена совпадут пренебрежительно мала: $1/64^{16} = 2^{-96} \approx 10^{-29}$.

Мы планируем хранить сам base64 токен в браузере, а его зашифрованную версию - в базе данных приложения. После чего мы сможем осуществлять логин пользователей вытягивая токен из куки, шифруя его, а затем ища ему соответствие в зашифрованных токенах хранимых в базе данных. Причина по которой мы храним только зашифрованные токены заключается в том, что, даже если вся наша база данных будет скомпрометирована, атакер все равно не сможет использовать токены для входа. Для того чтобы сделать наш токен еще более безопасными, мы планируем менять его каждый раз когда пользователь создает новую сессию, а это означает что любые похищенные сессии—когда атакер использует украденные куки для входа от лица определенного пользователя—истекут при следующем входе пользователя. (Похищение сессий получило широкую огласку с помощью приложения Firesheep, которое показывало что токены на множестве знаменитых сайтов были видимы при подключении к публичным Wi-Fi сетям. Решение заключается в использовании SSL повсеместно на сайте, как это было описано в Разделе 7.4.4.)

Хотя в реальном приложении мы будем немедленно 'входить' вновь созданного пользователя (тем самым создавая новый токен в качестве побочного эффекта), мы не будем полагаться на такое поведение; более надежная практика заключается в обеспечении каждого пользователя валидным токеном с самого начала. Для того чтобы достигнуть этого мы будем создавать начальный токен с помощью функции обратного вызова - техники впервые представленной в Разделе 6.2.5 в контексте уникальности адресов электронной почты. В том разделе мы использовали коллбэк before_save; в этот раз для создания remember_token непосредственно перед сохранением пользователя мы будем использовать очень похожий коллбэк before_create.3Более подробно о видах коллбэков, поддерживаемых библиотекой Active Record см. в обсуждении коллбэков в Rails Guides (# см. перевод на rusrails.ru).

Для того чтобы протестировать remember token, мы вначале сохраним тестового пользователя, а затем проверим, что атрибут пользовательского remember_token не является пустым. Что позволит нам при необходимости изменять случайную строку, если это нам когда-либо потребуется. Результат представлен в Листинге 8.17.

require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }
  .
  .
  .
  describe "remember token" do
    before { @user.save }
    its(:remember_token) { should_not be_blank }
  end
end
Листинг 8.17. Тест на валидный (не пустой) remember token. spec/models/user_spec.rb

Листинг 8.17 вводит метод its, который похож на it но относит следующий за ним тест к данному атрибуту, а не к субъекту теста. Другими словами,

its(:remember_token) { should_not be_blank }

является эквивалентом

it { expect(@user.remember_token).not_to be_blank }

Код приложения вводит несколько новых элементов в модель User (app/models/user.rb). Во-первых, мы добавили метод обратного вызова для создания remember token непосредственно перед созданием нового пользователя в базе данных:

before_create :create_remember_token

Этот код, называемый method reference понужает Rails искать метод с названием create_remember_token и выполнять его перед сохранением пользователя. (В Листинге 6.20 мы явно передавали блок в before_save, но техника ссылки на метод более предпочтительна в общем случае.) Во-вторых, сам метод используется тольку внутри модели User, так что нам нет необходимости выставлять его на показ сторонним пользователям. Как мы видели в Разделе 7.3.2, Ruby предлагает использовать для этих целей ключевое слово private:

private

  def create_remember_token
    # Create the token.
  end

Все методы, определенные в классе после private автоматически становятся скрытыми, таким образом

$ rails console
>> User.first.create_remember_token

вызовет исключение NoMethodError.

Наконец, метод create_remember_token необходимо присвоить одному из атрибутов пользователей и в этом контексте необходимо использовать ключевое слово self перед remember_token:

 def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def User.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  private

    def create_remember_token
      self.remember_token = User.encrypt(User.new_remember_token)
    end

Из-за способа которым Ruby обрабатывает назначения внутри объектов, без self назначение создаст локальную переменную с именем remember_token, а это совсем не то что нам нужно. Использование self обеспечивает установку назначением пользовательского remember_token таким образом, что он будет записан в базу данных вместе с другими атрибутами при сохранении пользователя. (Теперь вы знаете почему остальные before_save коллбэки из Листинга 6.20 используют self.email вместо просто email.)

Обратите внимание: мы шифровали токен с помощью SHA1 - хэширующего алгоритма который намного быстрее чем алгоритм Bcrypt используемый нами для шифрования паролей пользователей в Разделе 6.3.1, что важно, поскольку (как мы увидим в Разделе 8.2.2) для вошедших пользователей он будет выполняться на каждой странице. SHA1 является менее безопасным чем Bcrypt, но в данном случае его более чем достаточно так как шифруемый токен уже является 16-значной случайной строкой; SHA1 hexdigest такой строки по сути является невзламываемым. (Вызов to_s нужен для того чтобы мы имели возможность работать с nil токенами - этого не должно происходить в браузерах, но иногда может случаться в тестах.)

Методы encrypt и new_remember_token прикреплены к классу User так как для работы им не нужен инстанс пользователя4Если методу не нужен экземпляр объекта, он должен быть методом класса. и они являются публичными методами (выше строки private) поскольку в Разделе 8.2.3 мы будем их использовать за пределами модели User.

Собрав все это воедино мы приходим модели User показанной в Листинге 8.18.

class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  before_create :create_remember_token
  .
  .
  .
  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def User.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  private

    def create_remember_token
      self.remember_token = User.encrypt(User.new_remember_token)
    end
end
Листинг 8.18. Обратный вызов before_create для создания remember_token. app/models/user.rb

Кстати, дополнительный уровень отступа на create_remember_token сделан для того, чтобы визуально отделить методы определенные после private. (Практика показала что это мудрая практика.)

Поскольку шифрованная строка SecureRandom.urlsafe_base64 определенно не пустая, тесты для модели User теперь должны пройти:

$ bundle exec rspec spec/models/user_spec.rb
Вадим Обозин
Вадим Обозин

Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора?