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

Войти, выйти

Рабочий метод sign_in

Теперь мы готовы к написанию первого элемента входа - самой sign_in функции. Как было отмечено выше, выбранный нами метод аутентификации заключается в помещении (вновь созданного) remember token в качестве куки в браузер пользователя и последующем использовании токена для поиска записи пользователя в базе данных при перемещении пользователя от страницы к странице (реализовано в Разделе 8.2.3). Результирующий Листинг 8.19 вводит новый для нас метод current_user который мы будем реализовывать в Разделе 8.2.3.

module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end
end
Листинг 8.19. Завершенная (но пока-еще-не-работающая) функция sign_in. app/helpers/sessions_helper.rb

Здесь мы следуем избранной тактике: во-первых, создаем новый токен; во-вторых, помещаем зашифрованный токен в куки браузера; в-третьих, сохраняем зашифрованный токен в базе данных; в-четвертых, устанавливаем текущего пользователя равным данному пользователю (Раздел 8.2.3). Как мы увидим в Разделе 8.2.3, установка текущего пользователя равным user в данный момент не нужна из-за незамедлительного редиректа в create действии (Листинг 8.13), но все же это хорошая идея - на тот случай если мы когда-нибудь захотим использовать sign_in без редиректа.

В Листинге 8.19 обратите внимание на использование update_attribute для сохранения токена. Как вкратце упоминалось в Разделе 6.1.5), этот метод позволяет обновлять один атрибут в обход валидаций — в данном случае это необходимо так как у нас нет пароля пользователя. Листинг 8.19 также вводит утилиту cookies которая позволяет нам манипулировать куками браузера как если бы они были хэшем; каждый элемент в куки представляет из себя хэш из двух элементов: value и (необязательный) expires дата (# дата истечения). Например, мы могли бы осуществить вход пользователя путем размещения куки со значением, равным пользовательскому токену, которая истекает через 20 лет:

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

(Этот код использует один из удобных Rails помощников, о чем говорится в Блоке 8.1.)

______________________________________________________________

Блок 8.1.Куки истекают через 20.years.from_now

Вы можете вспомнить из Раздела 4.4.2, что Ruby позволяет добавлять методы к любому, даже встроенному классу. В том разделе мы добавляли palindrome? метод к String классу (и в результате обнаружили, что "deified" является палиндромом), и мы также видели, как Rails добавляет blank? метод к классу Object (таким образом, "".blank?, " ".blank?, и nil.blank? все являются true). Код куки в Листинге 8.19 (который внутренне устанавливает срок действия cookie в 20.years.from_now) дает еще один пример из этой практики, посредством одного из Rails’ временных хелперов, которые являются методами добавленными к Fixnum (базовый класс для чисел):

$ rails console
  >> 1.year.from_now
  => Sun, 13 Mar 2011 03:38:55 UTC +00:00
  >> 10.weeks.ago
  => Sat, 02 Jan 2010 03:39:14 UTC +00:00

Rails добавляет и другие помощники:

  >> 1.kilobyte
  => 1024
  >> 5.megabytes
  => 5242880

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

Хотя она должна использоваться с осторожностью, возможность добавлять методы к встроенным классам позволяет создавать черезвычайно естественные добавления к обычному Ruby. Действительно, большая часть элегантности Rails в конечном счете, является производной от податливости лежащего в его основе языка Ruby.

______________________________________________________________

Паттерн установки куки истекающей через 20 лет стал настолько общепринятым, что Rails добавил специальный метод permanent для его реализации, так что мы можем просто написать

cookies.permanent[:remember_token] = remember_token

Под капотом, применение permanent приводит к автоматической установке даты истечения куки через 20 лет (20.years.from_now).

После того как куки установлены, на последующих представлениях страниц мы можем извлекать пользователя с кодом вроде

User.find_by(remember_token: remember_token)

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

Текущий пользователь

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

module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end
end

Единственный участок кода который в данный момент не работает это:

self.current_user = user

Как было отмечено сразу после Листинга 8.19, этот код никогда не будет использоваться в данном приложении из-за немедленного редиректа в Листинге 8.13, но для метода sign_in было бы опасным полагаться на это.

Целью current_user, доступного и в контроллерах и в представлениях является возможность создания конструкции подобные этой:

<%= current_user.name %<

и

redirect_to current_user

Использование self в назначении является необходимым по тем же причинам что были отмечены в обсуждении приведшем к Листингу 8.18: без self Ruby будет просто создавать локальную переменную с названием current_user.

Для того, чтобы начать писать код для current_user, обратите внимание, что строка

self.current_user = user

это назначение, которое мы должны определить. В Ruby есть специальный синтаксис для определения таких назначаемых функций, показанный в Листинге 8.20.

module SessionsHelper

  def sign_in(user)
    .
    .
    .
  end

  def current_user=(user)
    @current_user = user
  end
end
Листинг 8.20. Определение назначения current_user. app/helpers/sessions_helper.rb

Это может выглядеть сбивающим с толку — большинство языков не позволит вам использовать знак равенства в определении метода, но это просто определение метода current_user= специально разработанного для обработки назначения current_user. Другими словами, код

self.current_user = ...

автоматически конвертируется в

current_user=(...)

тем самым вызывая метод current_user=. Его единственный аргумент это то, что находится справа от назначения, в данном случае - пользователь который войдет. Однострочный метод в теле просто устанавливает переменную экземпляра @current_user, эффективно хранящую пользователя для дальнейшего использования.

В обычном Ruby, мы могли бы определить второй метод, current_user, предназначенный для возвращения значения @current_user, как это показано в Листинге 8.21.

module SessionsHelper

  def sign_in(user)
    .
    .
    .
  end

  def current_user=(user)
    @current_user = user
  end

  def current_user
    @current_user     # Useless! Don't use this line.
  end
end
Листинг 8.21. Заманчивое, но бесполезное определение current_user.

Если бы мы сделали это, мы бы фактически повторили функциональность attr_accessor, который мы видели в Разделе 4.4.5.5На самом деле, эти двое абсолютно эквивалентны; attr_accessor это просто удобный способ создавать такие getter/setter методы автоматически. Проблема в том, что он совершенно не в состоянии решить наши проблемы: с кодом в Листинге 8.21, статус вошедшего пользователя будет забыт: как только пользователь перейдет на другую страницу — poof! — сессия закончится и пользователь автоматически выйдет. Это связано с тем что в HTTP отсутствует сохранение промежуточного состояния между парами "запрос-ответ" (Раздел 8.2.1) — когда пользователь делает второй запрос, все переменные устанавливаются к своим дефолтным значениям, в случае переменных экземпляра вроде @current_user это nil. Таким образом, когда пользователь обратится к еще одной странице, даже находясь в том же приложении, Rails установит @current_user равным nil и код в Листинге 8.21 не сделает то чего вы от него ожидали.

Для того чтобы избежать этой проблемы мы можем искать пользователя соответствующего remember token созданному кодом в Листинге 8.19, как это показано в Листинге 8.22. Обратите внимание: поскольку токен хранимый в базе данных зашифрован, нам нужно зашифровать токен полученный из куки прежде чем использовать его для поиска пользователя в базе данных. Мы достигним этого с помощью метода User.encrypt определенного в Листинге 8.18.

module SessionsHelper
  .
  .
  .
  def current_user=(user)
    @current_user = user
  end

  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end
end
Листинг 8.22. Поиск текущего пользователя с помощью remember_token. app/helpers/sessions_helper.rb

Листинг 8.22 использует общепринятый, но изначально обескураживающий ||= ("или равно") оператор присваиваивания (Блок 8.2). Его эффект заключается в установке переменной экземпляра @current_user пользователю, соответствующему remember token, но только если @current_user не определен.6Как правило, это означает присвоение переменных, которые изначально nil, но обратите внимание - ложные (false) значения также будут переписаны оператором ||= Иными словами, конструкция

@current_user ||= User.find_by(remember_token: remember_token)

вызывает метод find_by при первом вызове которого вызывается current_user, но при последующих вызовах возвращается @current_user без обращения к базе данных.7то является примером мемоизации, которая обсуждалась ранее в Блоке 6.3. Это полезно лишь в случае если current_user используется чаще чем один раз для запроса отдельно взятого пользователя; в любом случае, find_by будет вызван по крайней мере один раз при каждом посещении страницы на этом сайте.

______________________________________________________________

Блок 8.2.Что за *$@! этот ваш ||= ?

Конструкция ||= - очень Рубишная — то есть, она очень характерна для языка Ruby — и, следовательно, важно ее знать, если вы планируете много программировать на Ruby. Хотя на первый взгляд она может показаться таинственной, или равно легко понять по аналогии.

Начнем с общепринятой идиомы для изменения определенной в настоящее время переменной. Многие компьютерные программы включают приращение переменной, как в

x = x + 1

Большинство языков обеспечивают синтаксическое сокращение для этой операции; в Ruby (и в C, C++, Perl, Python, Java, и т.д.), это выглядит следующим образом:

x += 1

Аналогичные конструкции существуют и для других операторов:

$ rails console
  >> x = 1
  => 1
  >> x += 1
  => 2
  >> x *= 3
  => 6
  >> x -= 7
  => -1

В каждом случае, паттерном является то, что x = x O y и x O= y эквивалентны для любого оператора O.

Другим распространенным Ruby паттерном является назначение переменной, если она nil но в противном случае оставляя ее в покое. Вспоминая or оператор || из Раздела 4.2.3, мы можем записать это следующим образом:

>> @user
  => nil
  >> @user = @user || "the user"
  => "the user"
  >> @user = @user || "another user"
  => "the user"

Поскольку nil ложно в булевом контексте, первое присвоение это nil || "the user", что оценивается как "the user"; аналогично, второе присвоение является "the user" || "another user", которое также оценивается как "the user" — так как строки true в булевом контексте, серия || выражений прекращается после оценки первого выражения. (Эта практика оценки выражений || слева направо и остановки на первом истинном значении, известна как оценка короткого замыкания (short-circuit evaluation).)

Сравнивая в консольной сессии различные операторы, мы видим, что @user = @user || value следует x = x O y паттерну с || вместо O, что позволяет предположить следующую эквивалентную конструкцию:

>> @user ||= "the user"
  => "the user"

Вуаля!

______________________________________________________________

Вадим Обозин
Вадим Обозин

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