Глава 9 Войти, выйти
Теперь, когда новые пользователи могут регистрироваться на нашем сайте (Глава 8), пришло время дать зарегистрированным пользователям возможность входить на сайт и выходить из него. Это позволит нам добавить настройки, зависящие от регистрационного статуса и личности текущего пользователя. Например, в этой главе мы добавим в header сайта ссылки войти/выйти и ссылку на профиль пользователя; в Главе 11 мы будем использовать идентификацию вошедшего в систему пользователя для создания микросообщений, связанных с этим пользователем и в Главе 12 мы позволим текущему пользователю следовать за другими пользователями приложения (тем самым получать поток (feed) их микросообщений).Наличие функции входа пользователей в систему также позволит нам реализовать модель безопасности, ограничивающую доступ к определенным страницам, основываясь на идентификации вошедшего в систему пользователя. Например, как мы увидим в Главе 10, только вошедшие пользователи смогут получить доступ к странице, используемой для редактирования информации о пользователе. Система входа/выхода также позволит реализовать особые привилегии для пользователей с правами администратора, такие как возможность (также в Главе 10) удалять пользователей из базы данных.
Как и в предыдущих главах, мы будем делать нашу работу в новой ветке и объединим изменения в конце:
$ git checkout -b sign-in-out
9.1 Сессии
Сессия это полупостоянное соединение между двумя компьютерами, такими как клиентский компьютер с запущенным веб-браузером и сервер с запущенными на нем Rails. Есть несколько моделей поведения сессий, принятых в сети: “забывание” сессии при закрытии браузера, опциональное использование “запомнить меня” флажка для постоянных сессий, и запоминание сессий до явных признаков выхода пользователя из системы.1 Мы выберем последнюю из этих опций: когда пользователь войдет, мы запомним его статус вошедшего “навсегда”2 и очистим сесию только после явного выхода пользователя из системы.Удобно моделировать сессии как RESTful ресурс: у нас будет signin страница для
new сессий, вход будет создавать (create) сессию, и выход будет уничтожать (destroy) ее. Поэтому нам понадобится контроллер Sessions с new, create, и destroy
действиями. В отличие от контроллера Users, который использует сервер
базы данных (с помощью модели User) для сохранения данных, контроллер
Sessions будет использовать куки,
которые представляют собой небольшой фрагмент текста, помещаемого в
браузер пользователя. Большая часть работы, по созданию вход/выход
системы, происходит от построения этой, основанной на куках,
аутентификационной машинерии. В этом, а также в последующих разделах, мы
выполним подготовку к ее запуску, построив контроллер Sessions, формы
входа, и связанные действия контроллера. (Большая часть этой работы
параллельна регистрации пользователя из Главы 8.) Затем мы завершим вход пользователей необходимым куки-манипулирующим кодом в Разделе 9.3.9.1.1 Sessions контроллер
Элементы системы входа и выхода соответствуют определенным REST действиям Sessions контроллера: форма входа обрабатываетсяnew действием (рассматривается в этом разделе), сам вход обрабатывается отправкой запроса POST к действию create (Раздел 9.2 и Раздел 9.3) и выход обрабатывается отправкой запроса DELETE к действию destroy (Раздел 9.4). (Вспомним о соответствии между глаголами HTTP и REST действиями из Таблицы 6.2.) Так как мы знаем, что нам нужно new действие, мы можем создать его при генерации контроллера Sessions (аналогично контроллеру Users в Листинге 5.23):3$ rails generate controller Sessions new
$ rm -rf spec/views
$ rm -rf spec/helpers
new действия и соответствующего представления в новый файл Sessions контроллер specs (Листинг 9.1). (Эта схема должна показаться вам знакомой.)
Листинг 9.1. Тесты для
new сессия действия и представления. spec/controllers/sessions_controller_spec.rb
require 'spec_helper'
describe SessionsController do
render_views
describe "GET 'new'" do
it "should be successful" do
get :new
response.should be_success
end
it "should have the right title" do
get :new
response.should have_selector("title", :content => "Sign in")
end
end
end
new действия; также, пока мы здесь, мы создадим все действия, необходимые в этой главе. Мы в основном следуем примеру из Листинга 6.26, но в данном случае мы определим только конкретно нужные нам действия, т.е., new, create, и destroy, и также добавим именованные маршруты для входа и выхода (Листинг 9.2).
Листинг 9.2. Добавление ресурса для получения стандартных RESTful действий для сессий.
config/routes.rb
SampleApp::Application.routes.draw do
resources :users
resources :sessions, :only => [:new, :create, :destroy]
match '/signup', :to => 'users#new'
match '/signin', :to => 'sessions#new'
match '/signout', :to => 'sessions#destroy'
.
.
.
end
resources может принимать хэш опций, который в данном случае имеет ключ :only и значение, эквивалентное массиву действий, на которые реагирует контроллер Sessions. Ресурсы, определенные в Листинге 9.2 обеспечивают URL-адреса и действия, аналогичные ресурсу Users (Таблица 6.2), как видно в Таблице 9.1.| HTTP запрос | URL | Именованный маршрут | Действие | Цель (назначение) |
|---|---|---|---|---|
| GET | /signin | signin_path | new | страница для новой сессии (вход) |
| POST | /sessions | sessions_path | create | создание новой сессии |
| DELETE | /signout | signout_path | destroy | удаление сессии (выход) |
Таблица 9.1: RESTful маршруты, обеспеченные правилами сессий в Листинге 9.2.
new действие, как показано в Листинге 9.3 (который также определяет create и destroy действия для использования в будущем).
Листинг 9.3. Добавление заголовка для страницы входа.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
@title = "Sign in"
end
def create
end
def destroy
end
end
9.1.2 Форма для входа
Форма входа (или, что эквивалентно, форма новой сессии) похожа на форму регистрации, за исключением двух полей (адрес электронной почты и пароль) вместо четырех. Макет представлен в Рис. 9.1.
Рисунок 9.1: Макет формы входа. (полный размер)
form_for, принимающий в качестве аргумента переменную экземпляра @user:<%= form_for(@user) do |f| %>
.
.
.
<% end %>
@user. Это означает, что при построении формы новой сессии мы должны дать form_for немного больше информации; в частности, в то время какform_for(@user)
action формы должны POST в URL /users, в случае сессий, мы должны указать и имя ресурса, и соответствующий URL:form_for(:session, :url => sessions_path)
Листинг 9.4. Код для формы входа.
app/views/sessions/new.html.erb
<h1>Sign in</h1>
<%= form_for(:session, :url => sessions_path) do |f| %>
<div class="field">
<%= f.label :email %><br />
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password %>
</div>
<div class="actions">
<%= f.submit "Sign in" %>
</div>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
Рисунок 9.2: Форма входа (/sessions/new). (полный размер)
Листинг 9.5. HTML формы входа, произведенный Листингом 9.4.
<form action="/sessions" method="post">
<div class="field">
<label for="session_email">Email</label><br />
<input id="session_email" name="session[email]" size="30" type="text" />
</div>
<div class="field">
<label for="session_password">Password</label><br />
<input id="session_password" name="session[password]" size="30"
type="password" />
</div>
<div class="actions">
<input id="session_submit" name="commit" type="submit" value="Sign in" />
</div>
</form>
params, где params[:session][:email] и params[:session][:password]
соответствуют email и password полям. Обработка этой отправки — и, в
частности, аутентификация пользователей, основанная на отправленных
email и password — является целью следующих двух разделов.9.2 Сбой входа
Как и в случае создания пользователей (регистрации), первый шаг в создании сессий (вход) состоит в обработке неверного ввода. Мы начнем с рассмотрения того, что происходит при отправке формы, а затем организуем появление полезных сообщений об ошибке, в случае неудачного входа на сайт (как показано в Рис. 9.3.) Наконец, мы заложим основу для успешного входа (Раздел 9.3) путем оценки предоставляемой при входе информации на предмет валидности комбинации email/password.
Рисунок 9.3: Макет сбоя входа. (полный размер)
9.2.1 Обзор отправки формы
Давайте начнем с определения минималистичногоcreate действия для контроллера Sessions (Листинг 9.6), которое не делает ничего, кроме воспроизведения new представления. Отправка /sessions/new формы с чистыми полями дает результат, показаный на Рис. 9.4.
Листинг 9.6. Предварительная версия Sessions
create действия. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
render 'new'
end
.
.
.
end
Рисунок 9.4: Начальный сбой входа, с
create как в Листинге 9.6. (полный размер)params, содержащему email и password под ключом :session:--- !map:ActiveSupport::HashWithIndifferentAccess
commit: Sign in
session: !ActiveSupport::HashWithIndifferentAccess
password: ""
email: ""
authenticity_token: BlO65PA1oS5vqrv591dt9B22HGSWW0HbBtoHKbBKYDQ=
action: create
controller: sessions
params содержит вложенный хэш формы{ :session => { :password => "", :email => "" } }
params[:session]
{ :password => "", :email => "" }
params[:session][:email]
params[:session][:password]
Иными словами, внутри
create действия хэш params
имеет всю информацию, необходимую для аутентификации пользователей по
электронной почте и паролю. Совершенно не случайно у нас уже как раз
есть необходимый нам метод: User.authenticate из Раздела 7.2.4 (Листинг 7.12). Напомним, что authenticate возвращает nil для невалидной аутентификации, нашу стратегию для входа пользователей можно резюмировать следующим образом:def create
user = User.authenticate(params[:session][:email],
params[:session][:password])
if user.nil?
# Create an error message and re-render the signin form.
else
# Sign the user in and redirect to the user's show page.
end
end
9.2.2 Ошибка входа (тест и код)
Для того, чтобы обрабатывать неудачную попытку входа, сначала нужно определить, что она неудачная. Тесты будут следовать примеру аналогичных тестов для регистрации пользователей (Листинг 8.6), как показано в Листинге 9.7.
Листинг 9.7. Тесты для неудачной попытки входа.
spec/controllers/sessions_controller_spec.rb
require 'spec_helper'
describe SessionsController do
render_views
.
.
.
describe "POST 'create'" do
describe "invalid signin" do
before(:each) do
@attr = { :email => "email@example.com", :password => "invalid" }
end
it "should re-render the new page" do
post :create, :session => @attr
response.should render_template('new')
end
it "should have the right title" do
post :create, :session => @attr
response.should have_selector("title", :content => "Sign in")
end
it "should have a flash.now message" do
post :create, :session => @attr
flash.now[:error].should =~ /invalid/i
end
end
end
end
params , а затем передаем их методу User.authenticate . Если пользователь не прошел проверку подлинности (то есть, если это nil), устанавливаем заголовок и повторно выводим форму входа.4 Мы обработаем другую часть выражения if-else в Разделе 9.3; на данный момент ограничившись комментарием описывающим наши планы.
Листинг 9.8. Код для неудачной попытки входа на сайт.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
user = User.authenticate(params[:session][:email],
params[:session][:password])
if user.nil?
flash.now[:error] = "Invalid email/password combination."
@title = "Sign in"
render 'new'
else
# Sign the user in and redirect to the user's show page.
end
end
.
.
.
end
flash.now; см. Блок 9.1). Благодаря отображению флэш сообщений в макете сайта (Листинг 8.16), flash[:error] сообщения отображаются автоматически; благодаря Blueprint CSS, они автоматически приобретают приятный стиль (Рис. 9.5).
Рисунок 9.5: Неудачная попытка входа (с флэш сообщением). (полный размер)
9.3 Успешный вход
Получив обработку неудачного входа, теперь нам нужно на самом деле впустить пользователя. Намек на то, куда мы двигаемся — страница профиля пользователя, с измененными навигационными ссылками — представлена в макете на Рис. 9.6.5 Получение этого результата потребует самого сложного Ruby программирования, которое мы когда либо встречали в этом учебнике, так что держитесь до конца и будьте готовы к небольшому количеству тяжелой работы. К счастью, первый шаг прост — завершениеcreate действия контроллера Sessions — простая задача. К сожалению, эта легкость обманчива.
Рисунок 9.6: Макет профиля пользователя после успешного входа (с обновленными навигационными ссылками). (полный размер)
9.3.1 Завершение create действия
Заполнить область, занятую в настоящее время комментарием (Листинг 9.8) легко: после успешного входа, мы впускаем пользователя, используя функцию sign_in, а затем перенаправляем его на страницу профиля (Листинг 9.9). Мы видим теперь, почему это обманчивая легкость: увы, sign_in в настоящее время не существует. Написание этой функции займет оставшуюся часть этого раздела.
Листинг 9.9. Завершенное действие
create контроллера Sessions (пока не рабочее). app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
user = User.authenticate(params[:session][:email],
params[:session][:password])
if user.nil?
flash.now[:error] = "Invalid email/password combination."
@title = "Sign in"
render 'new'
else
sign_in user
redirect_to user
end
end
.
.
.
end
sign_in функции, мы все же можем написать тесты (Листинг 9.10). (Мы заполним тело первого теста в Разделе 9.3.3.)
Листинг 9.10. Ожидающие тесты для входа пользователя (будут завершены в Разделе 9.3.3).
spec/controllers/sessions_controller_spec.rb
describe SessionsController do
.
.
.
describe "POST 'create'" do
.
.
.
describe "with valid email and password" do
before(:each) do
@user = Factory(:user)
@attr = { :email => @user.email, :password => @user.password }
end
it "should sign the user in" do
post :create, :session => @attr
# Fill in with tests for a signed-in user.
end
it "should redirect to the user show page" do
post :create, :session => @attr
response.should redirect_to(user_path(@user))
end
end
end
end
9.3.2 Запомнить меня
Мы теперь в состоянии приступить к реализации нашей модели входа, а именно, запоминанию статуса вошедшего пользователя “навсегда” и очистке сессии только тогда, когда пользователь явно покинет наш сайт. Сами функции входа, в конечном итоге, пересекают традиционное Модель-Представление-Контроллер; в частности, несколько функций входа должны быть доступны и в контроллерах и в представлениях. Вы можете вспомнить из Раздела 4.2.5 что Ruby предоставляет модуль для упаковки функций вместе и включения их в нескольких местах, и это план для функций аутентификации. Мы могли бы сделать совершенно новый модуль для аутентификации, но контроллер Sessions уже оснащен модулем, а именно,SessionsHelper. Кроме того, помощники,
автоматически включаются в Rails представления, так что все что мы
должны сделать, чтобы использовать функции Sessions хелпера в
контроллерах, это включить соответствующий модуль в Application
controller (Листинг 9.11).
Листинг 9.11. Включение модуля Sessions хелпер в Application controller.
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
end
views, но не в контроллерах. Нам нужны методы Sessions хелпера в обоих местах, поэтому мы должны включить его в явном виде.Теперь мы готовы к первому элементу входа, самой
sign_in функции. Наш метод аутентификации заключается в помещении remember token как cookie в браузер пользователя (Блок 9.2),
а затем использовании token для поиска записи пользователя в базе
данных при перемещении пользователя от страницы к странице (реализовано в
Разделе 9.3.3). В результате, Листинг 9.12, отправляет в стек две вещи: хэш cookies и current_user.6 Let’s start popping them off.(??)
Листинг 9.12. Завершенная (но пока еще не работающая)
sign_in функция. app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
self.current_user = user
end
end
cookies, поставляемую Rails. Мы можем использовать cookies как если бы она была хэшем; каждый элемент в cookie это сам хэш из двух элементов, value и дополнительное expires
дата (срок)(# дата истечения). Например, мы могли бы осуществить вход
пользователя путем размещения cookie со значением, равным
пользовательскому id, которое истекает через 20 лет:cookies[:remember_token] = { :value => user.id,
:expires => 20.years.from_now.utc }
User.find_by_id(cookies[:remember_token])
cookies на самом деле не хэш, поскольку назначение cookies фактически сохраняет часть текста в браузере (как видно в Рис. 9.7),
но часть красоты Rails заключается в том, что он позволяет забыть об
этих подробностях и сконцентрироваться на написании приложений.
Рисунок 9.7: Безопасный remember token. (полный размер)
cookies.permanent.signed:cookies.permanent.signed[:remember_token] = [user.id, user.salt]
permanent является причиной, по которой Rails устанавливает истечение срока действия в 20.years.from_now, и signed
делает cookie безопасными, так что id пользователя никогда не будет
показан в браузере. (Мы увидим, как получить пользователя, используя
remember token в Разделе 9.3.3.)Код выше показывает важность использования
new_record? в Листинге 7.10
для сохранения соли только после создания пользователя. В противном
случае, соль менялась бы каждый раз, при сохранении пользователя, сделав
невозможным извлечение пользовательской сессии в Разделе 9.3.3.9.3.3 Текущий пользователь
В этом разделе мы узнаем, как получить и установить сессии для текущего пользователя. Давайте снова посмотрим наsign_in чтобы увидеть, где мы находимся:module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
self.current_user = user
end
end
self.current_user = user
current_user, доступного и в контроллерах и в представлениях, что позволит создавать конструкции, подобные этой:<%= current_user.name %>
redirect_to current_user
current_user.Для описания поведения оставшейся машинерии входа, мы сначала заполним тест для входа пользователя (Листинг 9.13).
Листинг 9.13. Заполнение теста для входа пользователя.
spec/controllers/sessions_controller_spec.rb
describe SessionsController do
.
.
.
describe "POST 'create'" do
.
.
.
describe "with valid email and password" do
before(:each) do
@user = Factory(:user)
@attr = { :email => @user.email, :password => @user.password }
end
it "should sign the user in" do
post :create, :session => @attr
controller.current_user.should == @user
controller.should be_signed_in
end
it "should redirect to the user show page" do
post :create, :session => @attr
response.should redirect_to(user_path(@user))
end
end
end
end
controller переменную (которая доступна внутри Rails тестов), чтобы проверить, что current_user переменная установлена для вошедшего пользователя, и что пользователь вошел:it "should sign the user in" do
post :create, :session => @attr
controller.current_user.should == @user
controller.should be_signed_in
end
controller.should be_signed_in
controller.signed_in?.should be_true
signed_in? метод, который возвращает true если пользователь вошел и false в противном случае. Кроме того, signed_in? метод будет прикреплен к контроллеру, а не к пользователю, поэтому мы пишем controller.signed_in? вместо current_user.signed_in?. (Если пользователь не вошел в систему, как бы мы вызвали signed_in? на это?)Чтобы начать писать код для
current_user, обратите внимание, что строкаself.current_user = user
Листинг 9.14. Определение присвоения
current_user. app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
.
.
.
end
def current_user=(user)
@current_user = user
end
end
current_user= специально разработанного для обработки назначения current_user.
Его единственный аргумент находящийся справа, в данном случае
пользователь, который войдет. Однострочный метод в теле просто
устанавливает переменную экземпляра @current_user, эффективно хранящую пользователя для дальнейшего использования.В обычном Ruby, мы могли бы определить второй метод,
current_user, предназначенный для возвращения значения @current_user (Листинг 9.15).
Листинг 9.15. Заманчивое, но бесполезное определение
current_user.
module SessionsHelper
def sign_in(user)
.
.
.
end
def current_user=(user)
@current_user = user
end
def current_user
@current_user # Бесполезно! Не используйте эту строку.
end
end
attr_accessor, впервые показанного в Разделе 4.4.5 и использовавшегося для создания виртуального атрибута password в Разделе 7.1.1.8 Проблема в том, что он совершенно не в состоянии решить наши проблемы: с кодом в Листинге 9.15, статус вошедшего пользователя будет забыт: как только пользователь перейдет на другую страницу — poof! — сессия закончится и пользователь автоматически выйдет!Чтобы избежать этой проблемы, мы можем найти сессию пользователя соответствующую cookie созданному кодом в Листинге 9.12, как показано в Листинге 9.16.
Листинг 9.16. Поиск текущего пользователя по
remember_token. app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
def current_user
@current_user ||= user_from_remember_token
end
private
def user_from_remember_token
User.authenticate_with_salt(*remember_token)
end
def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end
Во-первых, Листинг 9.16 использует общепринятый, но изначально обескураживающий
||= (“или равно”) оператор присваиваивания (Блок 9.4). Его эффект заключается в установке переменной экземпляра @current_user пользователю, соответствующему remember token, но только если @current_user не определен.9 Иными словами, конструкция@current_user ||= user_from_remember_token
user_from_remember_token при первом вызове которого вызывается current_user, но при последующих вызовах возвращается @current_user без вызова user_from_remember_token.10Листинг 9.16 также использует
* оператор,
что позволяет нам использовать два элемента массива в качестве
аргумента метода, ожидающего две переменные, как мы видим в этой
консольной сессии:$ rails console
>> def foo(bar, baz)
?> bar + baz
?> end
=> nil
>> foo(1, 2)
=> 3
>> foo(*[1, 2])
=> 3
cookies.signed[:remember_me]
возвращает массив из двух элементов — пользовательский id и соль — но
(в соответствии с общепринятой конвенцией Ruby) мы хотим, чтобы метод authenticate_with_salt принимал два аргумента, это может быть осуществлено следующим образом:User.authenticate_with_salt(id, salt)
authenticate_with_salt не мог бы принять массив в качестве аргумента не существует, но это был бы идиоматически некорректный Ruby.)Наконец, во вспомогательном методе
remember_token, определенном в Листинге 9.16, мы используем || оператор для возвращения массива с nil значениями, если cookies.signed[:remember_me] само является nil:cookies.signed[:remember_token] || [nil, nil]
nil значение для cookie может вызвать ложные поломки в тестах. Возвращая [nil, nil] вместо этого, мы решаем проблему.11Последним шагом для получения рабочего кода в Листинге 9.16 является определение
authenticate_with_salt метода класса. Этот метод, аналогичный оригинальному authenticate методу, определенному в Листинге 7.12, показан в Листинге 9.17.
Листинг 9.17. Добавление
authenticate_with_salt метода к модели User. app/models/user.rb
class User < ActiveRecord::Base
.
.
.
def self.authenticate(email, submitted_password)
user = find_by_email(email)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
def self.authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user : nil
end
.
.
.
end
authenticate_with_salt вначале ищет пользователя
по уникальному id, а затем проверяет что соль, хранящаяся в cookie
является правильной для этого пользователя.Стоит отметить, что эта реализация
authenticate_with_salt идентична по функционированию следующему коду, который более тесно параллелен authenticate методу:def self.authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
return nil if user.nil?
return user if user.salt == cookie_salt
end
user не nil, а соль пользователя совпадает с солью cookie’s, и возвращает nil в противном случае. С другой стороны, код, подобный(user && user.salt == cookie_salt) ? user : nil
if-else конструкции в одну строку (Box 9.5).В данный момент, тест входа почти проходит; осталось лишь определить необходимый булев метод
signed_in? К счастью, это легко сделать с помощью “не” оператора !: пользователь является вошедшим, если current_user не является nil (Листинг 9.18).
Листинг 9.18. Вспомогательный метод
signed_in?. app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
def signed_in?
!current_user.nil?
end
private
.
.
.
end
signed_in? лучшее применение в Разделе 9.4.3, а затем в Главе 10.С этим все тесты должны пройти.
9.4 Выход
Как обсуждалось в Разделе 9.1, наша аутентификационная модель предполагает сохранение пользователей вошедшими до тех пор, пока они явно не выйдут из системы. В этом разделе мы добавим эту необходимую возможность выхода. Как только мы закончим с этим, мы добавим интеграционные тесты и заставим нашу аутентификационную машинерию пройти их.9.4.1 Уничтожение сессий
До сих пор действия контроллера Sessions следовали RESTful конвенции, используяnew для страницы входа и create для его завершения. Мы продолжим эту тему используя действие destroy для удаления сессий, т.е., для выхода.Для того, чтобы протестировать действие выхода, нам, в первую очередь, необходим способ для входа в рамках теста. Самый простой способ сделать это, состоит в использовании объекта
controller, который мы видели в Разделе 9.3.3 и использование sign_in помощника для входа данного пользователя. Для того чтобы использовать полученную функцию test_sign_in во всех наших тестах, мы должны поместить ее в файл spec helper, как показано в Листинге 9.19.12
Листинг 9.19. Функция
test_sign_in для симуляции входа пользователя внутри тестов. spec/spec_helper.rb
.
.
.
RSpec.configure do |config|
.
.
.
def test_sign_in(user)
controller.sign_in(user)
end
end
test_sign_in, current_user не будет nil, таким образом, signed_in? будет true.С этим spec хелпером в руке, тест на выход прост: войти в систему как (сфабрикованный) пользователь, а затем обратиться к
destroy действию и убедиться, что пользователь вышел (Листинг 9.20).
Листинг 9.20. Тест для уничтожения сессии (выхода пользователя).
spec/controllers/sessions_controller_spec.rb
describe SessionsController do
.
.
.
describe "DELETE 'destroy'" do
it "should sign a user out" do
test_sign_in(Factory(:user))
delete :destroy
controller.should_not be_signed_in
response.should redirect_to(root_path)
end
end
end
delete метод, который выдает HTTP запрос DELETE (по аналогии с get и post методами, виденными в предыдущих тестах), в соответствии с требованиями конвенции REST (Таблица 9.1).Как и с входом пользователя, который опирается на функцию
sign_in, выход пользователя просто откладывает тяжелую работу в функцию sign_out (Листинг 9.21).
Листинг 9.21. Уничтожение сессии (выход пользователя).
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def destroy
sign_out
redirect_to root_path
end
end
sign_out в модуль Sessions хелпера (Листинг 9.22).
Листинг 9.22. Метод
sign_out в модуле Sessions хелпера. app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
self.current_user = user
end
.
.
.
def sign_out
cookies.delete(:remember_token)
self.current_user = nil
end
private
.
.
.
end
sign_out эффективно отменяет метод sign_in, удаляя remember token и устанавливая текущего пользователя равным nil.139.4.2 Вход после регистрации
В принципе, мы закончили с аутентификацией, но в настоящее время конструкцией приложения не предусмотрены ссылки на Вход и Выход действия. Кроме того, вновь зарегистрированные пользователи могут оказаться сбитыми с толку, так как они не вошли в систему по умолчанию.Мы исправим вторую проблему первой, начав с тестирования того, что новый пользователь автоматически входит в систему (Листинг 9.23).
Листинг 9.23. Тестирование того, что вновь зарегистрированные пользователи также являются вошедшими.
spec/controllers/users_controller_spec.rb
require 'spec_helper'
describe UsersController do
render_views
.
.
.
describe "POST 'create'" do
.
.
.
describe "success" do
.
.
.
it "should sign the user in" do
post :create, :user => @attr
controller.should be_signed_in
end
.
.
.
end
end
end
sign_in из Раздела 9.3, получить прохождения этого теста, фактически впустив пользователя в систему, легко: просто добавим sign_in @user сразу после сохранения пользователя в базе данных (Листинг 9.24).
Листинг 9.24. Вход пользователя сразу после регистрации.
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(params[:user])
if @user.save
sign_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
@title = "Sign up"
render 'new'
end
end
9.4.3 Изменение ссылок шаблона
Мы подходим, наконец, к практическому применению всей нашей войти/выйти работы: мы изменим ссылки в шаблоне, основываясь на статусе пользователя. В частности, как показано на макете в Рис. 9.6 мы организуем изменение ссылок, при входе и выходе пользователей из системы, и мы также добавим ссылку на профиль пользователя в страницу, показывающую пользователя для вошедших пользователей.Начнем с двух интеграционных тестов: один будет проверять, что ссылка
"Sign in" будет появляться для не вошедших пользователей, а другой будет проверять, что ссылка "Sign out"
будет появляться для вошедших пользователей; в обоих случаях будет
проверяться, что ссылка ведет к надлежащему URL. Мы поместим эти тесты в
тест ссылок макета, который мы создали в Разделе 5.2.1; результат представлен в Листинге 9.25.
Листинг 9.25. Тесты для ссылок Войти/Выйти шаблона сайта.
spec/requests/layout_links_spec.rb
describe "Layout links" do
.
.
.
describe "when not signed in" do
it "should have a signin link" do
visit root_path
response.should have_selector("a", :href => signin_path,
:content => "Sign in")
end
end
describe "when signed in" do
before(:each) do
@user = Factory(:user)
visit signin_path
fill_in :email, :with => @user.email
fill_in :password, :with => @user.password
click_button
end
it "should have a signout link" do
visit root_path
response.should have_selector("a", :href => signout_path,
:content => "Sign out")
end
it "should have a profile link"
end
end
before(:each) входит, посетив страницу входа и отправив валидную пару email/пароль.14 Мы делаем это вместо использования test_sign_in функции из Листинга 9.19 так как test_sign_in не работает внутри итеграционных тестов по ряду причин. (См. упражнение в Разделе 9.6, предлагающее сделать функцию integration_sign_in для использования в интеграционных тестах.)Код приложения использует если-то ветвящиеся структуры внутри Embedded Ruby, используя метод
signed_in?, определенный в Листинге 9.18:<% if signed_in? %>
<li><%= link_to "Sign out", signout_path, :method => :delete %></li>
<% else %>
<li><%= link_to "Sign in", signin_path %></li>
<% end %>
Листинг 9.26. Изменение ссылок макета для вошедших пользователей.
app/views/layouts/_header.html.erb
<header>
<%= link_to logo, root_path %>
<nav class="round">
<ul>
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if signed_in? %>
<li><%= link_to "Sign out", signout_path, :method => :delete %></li>
<% else %>
<li><%= link_to "Sign in", signin_path %></li>
<% end %>
</ul>
</nav>
</header>
logo хелпер из упражнения Главы 5 (Раздел 5.5); в случае, если вы не проработали это упражнение, ответ представлен в Листинге 9.27.
Листинг 9.27. Хелпер для логотипа сайта.
app/helpers/application_helper.rb
module ApplicationHelper
.
.
.
def logo
image_tag("logo.png", :alt => "Sample App", :class => "round")
end
end
current_user,16 что является нашим первым применением этого полезного метода. (Первым, но не последним.)
Листинг 9.28. Тест для ссылки на профиль пользователя.
spec/requests/layout_links_spec.rb
describe "Layout links" do
.
.
.
describe "when signed in" do
.
.
.
it "should have a profile link" do
visit root_path
response.should have_selector("a", :href => user_path(@user),
:content => "Profile")
end
end
end
Листинг 9.29. Добавление ссылки на профиль пользователя.
app/views/layouts/_header.html.erb
<header>
<%= link_to logo, root_path %>
<nav class="round">
<ul>
<li><%= link_to "Home", root_path %></li>
<% if signed_in? %>
<li><%= link_to "Profile", current_user %></li>
<% end %>
<li><%= link_to "Help", help_path %></li>
<% if signed_in? %>
<li><%= link_to "Sign out", signout_path, :method => :delete %></li>
<% else %>
<li><%= link_to "Sign in", signin_path %></li>
<% end %>
</ul>
</nav>
</header>
Рисунок 9.8: Вошедший пользователь со ссылками на Выход и профиль. (полный размер)
9.4.4 Интеграционный тест для входа и выхода
В качестве кульминации нашей тяжелой работы над аутентификацией, мы закончим интеграционными тестами для входа и выхода (поместив их в файлusers_spec.rb в соответствии с конвенцией). RSpec интеграционное тестирование достаточно выразительно само по себе, но все же Листинг 9.30 требует небольшого разъяснения; мне особенно нравится использование click_link "Sign out",
которое не только симулирует клик по ссылке Выход, но также выдает
ошибку, в случае если такая ссылка не существует — тем самым тестируя
URL, именованный маршрут, текст ссылки, и изменение ссылок шаблона, и
все в одной строке. Если это не интеграционный тест, то я не знаю, что еще может им быть.
Листинг 9.30. Интеграционный тест для входа и выхода.
spec/requests/users_spec.rb
require 'spec_helper'
describe "Users" do
describe "signup" do
.
.
.
end
describe "sign in/out" do
describe "failure" do
it "should not sign a user in" do
visit signin_path
fill_in :email, :with => ""
fill_in :password, :with => ""
click_button
response.should have_selector("div.flash.error", :content => "Invalid")
end
end
describe "success" do
it "should sign a user in and out" do
user = Factory(:user)
visit signin_path
fill_in :email, :with => user.email
fill_in :password, :with => user.password
click_button
controller.should be_signed_in
click_link "Sign out"
controller.should_not be_signed_in
end
end
end
end
9.5 Заключение
Мы очень многое узнали в этой главе, трансформируя наше многообещающее но не сформированное приложение в сайт, обладающий полным набором функций для регистрации и входа/выхода пользователей. Все что нам необходимо для завершения аутентификационной функциональности, это ограничить доступ к страницам по статусу и идентификации пользователей. Мы выполним эту задачу, по пути дав пользователям возможность редактировать их информацию, а также дав администраторам возможность удалять пользователей из системы.Прежде чем двигаться далее, объедините изменения с мастер веткой:
$ git add .
$ git commit -m "Done with sign in"
$ git checkout master
$ git merge sign-in-out
9.6 Упражнения
Второе и третье упражнения труднее, чем обычно. Их решение требует поиска во внешних источниках (например, чтения Rails API и поиска в Google), и они могут быть пропущены без особых потерь.- Несколько интеграционных тестов используют одинаковый код для входа пользователя. Замените этот код на функцию
integration_sign_inиз Листинга 9.31 и проверьте, что тесты проходят. - >Используйте
sessionвместоcookiesтак, чтобы пользователи автоматически выходили при закрытии браузера.17 Подсказка: Ищите в Google “Rails session” (Rails сессии). - (продвинутое) Некоторые сайты используют безопасный
HTTP (HTTPS) для своих страниц входа. Поищите в сети, как использовать
HTTPS в Rails, а затем обезопасьте
newиcreateдействия контроллера Sessions. Сверхзадача: Написать тесты для HTTPS функциональности. (Примечание: Я советую выполнять это упражнение только в development, что не потребует получения SSL сертификата или настройки SSL шифровальной машинерии. Как ни странно, развертывание сайтов с включенным SSL гораздо сложнее.)
Листинг 9.31. Функция для входа пользователей внутри интеграционных тестов.
spec/spec_helper.rb
.
.
.
RSpec.configure do |config|
.
.
.
def test_sign_in(user)
controller.sign_in(user)
end
def integration_sign_in(user)
visit signin_path
fill_in :email, :with => user.email
fill_in :password, :with => user.password
click_button
end
end
- Другой распространенной моделью является завершение сессии после истечения определенного количества времени. Это особенно уместно на сайтах, содержащих конфиденциальную информацию, такую как банковские и финансово-торговые операции. ↑
- В Разделе 9.3.2 мы увидим, насколько продолжительно это самое “навсегда”. ↑
- Если бы мы также добавили
createиdestroyдействия, generate скрипт создал бы представления для этих действий, которые нам не нужны. Конечно, мы могли бы удалить представления, но я предпочел не допустить их создания скриптомgenerate, а вместо этого определить действия вручную. ↑ - В случае, если вы задаетесь вопросом, почему мы используем
userвместо@userв Листинге 9.8, это связано с тем, что эта user переменная никогда более не понадобится в других представлениях, следовательно, нет оснований использовать здесь переменную экземпляра. (Хотя использование@userпо прежнему возможно.) ↑ - Изображение из http://www.flickr.com/photos/hermanusbackpackers/3343254977/. ↑
- На некоторых системах, вам, возможно, придется использовать
self.current_user = userдля прохождения предстоящих тестов. ↑ - Из-за включения модуля Sessions хелпера в Application controller,
selfпеременная здесь является самим контроллером. ↑ - Фактически, эти двое полностью эквивалентны;
attr_accessorэто просто удобный способ создать именно такие getter/setter методы автоматически. ↑ - Как правило, это означает присвоение переменных, которые изначально
nil, но не ложные (false) значения также будут переписаны оператором||=. ↑ - Этот метод оптимизации, позволяющий избежать повторных вызовов функции известен как мемоизация. ↑
- Это вызывает чувство, как будто хвост виляет собакой, но это цена, которую мы платим за использование передовых технологий. ↑
- Если вы используете Spork, она будет расположена внутри блока
Spork.prefork. ↑ - Вы можете узнать о вещах, подобных
cookies.delete, почитав введение в cookies в Rails API. (Так как ссылки на Rails API имеют тенденцию быстро устаревать, используйте ваш Google для поиска актуальной версии.) ↑ - Обратите внимание, что мы можем использовать символы вместо строк для этикеток, например,
fill_in :emailвместоfill_in "Email". Мы использовали последнюю в Листинге 8.22, но теперь вас не должно удивлять, что Rails позволяет использовать в этом месте символы. ↑ - Веб браузеры на самом деле не могут выдавать запрос DELETE; Rails подделывает его с помошью JavaScript. ↑
- Вспомните из Раздела 7.3.3 что мы можем сделать ссылку непосредственно на объект user и позволить Rails вывести соответствующий URL. ↑
- Несколько странно: мы использовали
cookiesдля реализации сессий, иsessionреализуются с cookies! ↑
Комментариев нет:
Отправить комментарий