Я уже давно обещал показать, как использовать решения для аутентификации в Ruby On Rails. И вот, вторая часть статьи про управление пользователями и Rails перед вашими глазами.
Подготовка
В этой статье мы не коснемся всех аспектов работы с пользователями. Мы не разработаем систему администрирования, наши пользователи не смогут получать оповещения и подтверждения регистрации по почте, аутентификация через openID тоже останется за бортом.
Мы будем использовать Shoulda для тестирования и authlogic для аутентификации пользователей.
Недавно в google группе ror2ru было обсуждение библиотек для аутентификации. Еще о них можно прочитать в одной из предыдущих статей. Authlogic – не самое простое решение, как правильно написал Макс Лапшин, не мало кода придется дописать, чтобы довести его до полноценной системы аутентификации и авторизации. Однако, authlogic развивается, уже скоро будут доступны дополнения к нему, позволяющие работать с openId, с ldap аутентификацией. За ними последуют дополнения и для всего остального, разве что, кофе он варить все-таки не сможет %)
Давайте сразу установим нужные библиотеки. Shoulda и Authlogic распространяются в виде gem-пакетов, для их установки достаточно подредактировать файлы config/environment.rb и config/environment/test.rb:
# config/environments/test.rb
config.gem 'mocha',
:version => '>= 0.9.5'
config.gem 'thoughtbot-factory_girl',
:lib => 'factory_girl',
:source => 'http://gems.github.com',
:version => '>= 1.2.0'
config.gem 'thoughtbot-shoulda',
:lib => 'shoulda',
:source => 'http://gems.github.com',
:version => '>= 2.10.1'
# config/environment.rb
config.gem 'authlogic'
Теперь достаточно выполнить пару команд, и нужные gem-пакеты будут установлены:
rake gems:install
rake gems:install RAILS_ENV=test
Заметили лишний gem? Мы не говорили о factory_girl — отличной библиотеке для создания динамических тестовых объектов от ThoughtBot. Если вы еще с ней не знакомы — прочитайте ее описание, мы будем использовать ее в наших тестах, но об этом чуть позже.
Многие ruby on rails разрботчики используют mocking / stubbing фреймворки в разработке тестов, но если вы не принадлежите к их числу — вам совершенно необходимо прочитать и о них.
К сожалению, в одной статье не описать всех этих технологий, но если в них что-то осталось непонятным — спросите об этом в комментариях!
Authlogic
Одним из основных преимущество authlogic называлось то, что authlogic не является генератором, но первое же наше действие будет вызовом генератора:
script/generate session user_session
На самом деле все не так страшно, этот генератор лишь создает модель сессии, в которой подключает модуль authlogic. Вся логика поведения сессий скрыта:
# app/models/user_session.rb
class UserSession < Authlogic::Session::Base
# Класс уже унаследован от Authlogic::Session::Base.
# Тут можно добавить необходимые правки конфигурации. Подробнее: AuthLogic::Session::Config
end
С моделью разобрались, теперь создадим контроллер:
script/generate controller user_sessions
В контроллере сессий вам понадобится ну как минимум три метода — один для формы входа, второй непосредственно для аутентификации, а третий — для выхода из системы. Вот какой драфт получился у меня в одном из проектов:
class UserSessionsController < ApplicationController
before_filter :require_no_user, :only => [:new, :create]
before_filter :require_user, :only => [:destroy, :not_authorized ]
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = I18n.translate "flashes.user_session.created"
redirect_to account_url
else
render :action => :new
end
end
def destroy
current_user_session.destroy
flash[:notice] = I18n.translate "flashes.user_session.destroyed"
redirect_back_or_default signin_url
end
end
Сразу добавим маршруты для RESTful сессий:
# config/routes.rb
map.resource :user_session
Чего не хватает в этом примере? Ну, во первых, я удалил метод not_authorized, о нем я расскажу в следующей статье. Во-вторых, мои методы всегда возвращают ответ в html формате, не предусматривая никакого API, но для примера, этого достаточно. А самое главное — я использовал несколько методов, о которых еще не рассказал: require_user, require_no_user, current_user, current_user_session. Их названия говорят сами за себя, не так ли? Забегая вперед, скажу, что они все будут дописаны в ApplicationController.
Теперь вплотную займемся моделью пользователя:
script/generate model user \
login:string \
email:string \
crypted_password:string \
password_salt:string \
persistence_token:string \
login_count:integer \
last_request_at:datetime \
last_login_at:datetime \
current_login_at:datetime \
last_login_ip:string \
current_login_ip:string
rake db:migrate
Тем самым мы создали в базе данных таблицу пользователей, подняли версию базы с учетом этой таблицы. Authlogic самостоятельно управляет моделью, от нас потребуется лишь предоставить ему простой интерфейс для аутентификации пользователей.
# app/models/user.rb
class User < ActiveRecord::Base
# даем authlogic управлять моделью!
acts_as_authentic
end
Acts_as_authentic добавит проверку полей (имя пользователя, электропочта, пароль, проверки на уникальность почты и имени пользователя), специфичные для модели пользователя методы для работы с паролем и многое другое.
Теперь пора перейти к контроллеру пользователей. Разберемся с маршрутизацией:
# config/routes.rb
map.resource :account, :controller => "users"
map.resources :users
Тут есть небольшая хитрость — при таком подходе account_path будет обрабатываться тем же контроллером, что и user_path(id). Сейчас нам не нужно реализовывать и то и другое в одном контроллере, давайте попробуем сделать набросок:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_filter :require_no_user, :only => [:new, :create]
before_filter :require_user, :only => [:show, :edit, :update]
def new
@user = User.new
end
def create
@user = User.new(params[:user])
if @user.save
flash[:notice] = "Account registered!"
redirect_back_or_default account_url
else
render :action => :new
end
end
def show
@user = @current_user
end
def edit
@user = @current_user
end
def update
@user = @current_user # makes our views "cleaner" and more consistent
if @user.update_attributes(params[:user])
flash[:notice] = "Account updated!"
redirect_to account_url
else
render :action => :edit
end
end
end
Ну вот, должно работать :) Осталось только дописано функционал в ApplicationController и сверстать шаблоны. Не буду приводить тут код шаблонов, он слишком прост и очевиден, а хороший пример есть на github.
Помните, мы говорили про методы, которые надо дописать в ApplicationController? Это один из ключевых моментов доработки authlogic до полноценного решения:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# не писать в логи пароли.
filter_parameter_logging :password, :password_confirmation
# А вот эти методы сделать видимыми из шаблонов.
helper_method :current_user_session, :current_user
private
def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
end
def current_user
return @current_user if defined?(@current_user)
@current_user = current_user_session &amp;&amp; current_user_session.user
end
def require_user
unless current_user
store_location
flash[:notice] = "You must be logged in to access this page"
redirect_to new_user_session_url
return false
end
end
def require_no_user
if current_user
store_location
flash[:notice] = "You must be logged out to access this page"
redirect_to account_url
return false
end
end
def store_location
session[:return_to] = request.request_uri
end
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
end
Готово!
Shoulda
Мы только что разработали очень простое приложение с системой аутентификации пользователей, но не написали ни строчки тестов. Я не стал нагружать самые основные моменты установки authlogic тестами, они в ней не нужны вовсе. Но вот контроллеры пользователей и сессий тестами покрыть нужно, иначе, когда они разрастутся, будет непонятно, что там на самом деле происходит.
Для примера, попробуем написать тесты для users_controller:
require 'test_helper'
class UserSessionsControllerTest < ActionController::TestCase
# Тестируем форму аутентификации
context "get new" do
# Которая должна работать только для неаутентифицированных пользователей.
context "being anonymous" do
setup do
logout
get :new
end
should_render_template :new
should_respond_with :success
should_have_form :user_session
end
# А если пользователь уже аутентифицирован - перебрасываем его на страницу его аккаунта.
# Иначе говоря, не разрешаем повторную регистрацию уже вошедшим в систему пользователям.
context "being logged in" do
setup do
login
get :new
end
should_redirect_to ":account"
should_set_the_flash_to "You must be logged out to access this page"
end
end
# LOGIN
# Аналогичное поведение для самой аутентификации с тем исключением,
# что контроллер должен сохранять сессию (производить аутентификацию)
context "logging in" do
context "being logged in" do
setup do
login
post :create
end
should_redirect_to ":account"
should_set_the_flash_to "You must be logged out to access this page"
end
context "being anonymous" do
context "with valid credentials" do
setup do
logout
# Требуем вызова сохранения сессии.
UserSession.any_instance.expects(:save).returns(true)
post :create
end
# после успешной аутентификации нас перекинут на наш аккаунт
should_redirect_to ":account"
end
# Или, если пользователь облажался, опять показываем ему форму.
context "supplying invalid credentials" do
setup do
logout
UserSession.any_instance.stubs(:save).returns(false)
post :create
end
should_respond_with :success
should_render_template :new
end
end
end
# ...
end
В этом коде некоторые вещи могут показаться странными: во-первых, это не традиционый Text::Unit::TestCase, этот тест использует Shoulda. Если context и should методы вызывают недоумение — стоит прочитать о них в документации к shoulda. Второе — методы, о которых мы еще не говорили: login и logout.
Эмуляция аутентификации — один из основных камней преткновения в разработке тестов. Методы login и logout — моя попытка решить эту проблему. Они далеки от идеала и не подойдут для cucumber и сложных приемочных тестов, прыгающих по нескольким контроллерам, но для простых функциональных тестов — этого хватает:
def current_user
# Создаем нового тестового пользователя
@current_user ||= Factory(:user)
end
def user_session
@user_session = mock
# заставляем сессию возвращать тестового пользователя
@user_session.stubs(:user).returns(current_user)
# authlogic временами вызывает current_user_session.record %)
@user_session.stubs(:record)
@user_session
end
def login
# Делаем вид, что у нас существует сессия :)
UserSession.stubs(:find).returns( user_session )
end
def logout
@user_session = nil
end
Для начала подойдет :) Если у вас есть желание как-то это доработать, или уже есть реализованные методы лучше — буду очень рад, если вы ими поделитесь в комментариях!
Резюме
Я уже давно работал над этой статьей. В одной статье очень сложно рассказать и про тестирование, и про аутентификацию и про авторизацию. Мы коснулись лишь очень малой части всего этого, но прочитав эту статью вы должны быть готовы написать систему аутентификации на authlogic и разрабатывать свое приложение дальше, используя shoulda.
Очень многое еще будет написано в следующих статьях:
- Разработка системы аутентификации по openID.
- Статусы пользователей: почтовые подтверждения регистрации пользователей, восстановление паролей.
- Авторизация и роли пользователей.
Если что-то осталось непонятным, или я пропустил что-то важное, то, пожалуйста, напишите мне об этом в комментариях! Я буду рад вашему мнению и постараюсь сделать следующие статьи более содержательными.