понеділок, 24 жовтня 2011 р.

Інтеграції Warden з Sinatra і DataMapper

В Інтернеті можна знайти багато статей на тему аутентифікації з допомогою Warden у Sinatra. Я не буду оригінальним, і напишу ще одну, щоб самому розібратися з особливостями цього інструменту. Ціллю цієї статті не є показати повністю готовий механізм аутентифікації. А лише продемонструвати основні можливості інтеграції Warden з Sinatra і Datamapper.

Warden - це Rack middleware, що забезпечує механізм аутентифікації для веб-додатків на Ruby.
Warden вимагає деяких налаштувань для Sinatra. Цього можна уникнути, використовуючи плагін Sinatra::Warden. Але ми ж не шукаємо простих шляхів.


Налаштування DataMapper

Для початку створимо модель і таблицю бази даних для зберігання імені користувача і паролю:
class User
  include DataMapper::Resource

  property :id,       Serial
  property :username, String, :unique => true
  property :password, String

end

Налаштування Rack

Повинен бути оголошений додаток для невдач, а також, які стратегії аутентифікації використовуватимуться по замовчуванню.
use Rack::Session::Cookie, :secret => "bla-bla-bla"

use Warden::Manager do |manager|
  manager.default_strategies :password
  manager.failure_app = FailureApp.new
end
class FailureApp
  def call(env)
    uri = env['REQUEST_URI']
    puts "failure #{env['REQUEST_METHOD']} #{uri}"
  end
end

Налаштування сесії

Одним з результатів використання будь-якого об'єкта, наприклад об'єкта User, є те, що ви повинні сказати Warden, як серіалізувати його у та із сесії. Це потрібно налаштувати:
Warden::Manager.serialize_into_session do |user|
  user.id
end

Warden::Manager.serialize_from_session do |id|
  User.get(id)
end
Після успішної аутентифікації в сесії буде зберігатися ідентифікатор користувача.

Стратегія аутентифікації

Стратегія у Warden містить логіку для перевірки аутентифікації запиту.
Warden::Strategies.add(:password) do
  def valid?
    params['username'] || params['password']
  end
  
  def authenticate!
    u = User.authenticate(params['username'], params['password'])
    u.nil? ? fail!('Could not login in') : success!(u)
  end
end
Вище ми оголосили стратегію під назвою :password, яка містить два стандартні методи #valid? і #authenticate!.

Необов'язковий метод #valid? діє в якості охорони для стратегії. Якщо ви не визначите його, стратегія завжди буде працювати. В іншому випадку стратегія виконуватиметься тільки якщо #valid? поверне значення true.
Стратегія, яку ми оголосили вище передбачає, що якщо переданий хоча б один з параметрів 'username' чи 'password', то користувач намагається увійти. Якщо є тільки один з ним, то далі виклик User#authenticate закінчиться невдачею.

У методі #authenticate! відбувається аутентифікація. Він містить логіку для перевірки аутентифікації запитів.

Тут же і містяться дві стандартні дії: fail! і success!.
success! встановлює успішну аутентифікацію. Екземпляром класу User серіалізується у сесію.
fail! встановлює, що стратегію пройшла безуспішно.

Для перевірки аутентифікації будемо використовувати відкритий метод User#authenticate, який повертатиме об'єкт User, якщо передане коректне ім'я користувача і пароль:
class User
  include DataMapper::Resource
  # ...

  def self.authenticate(username, password)
    user = first(:username => username)
    if user
      if user.password != password
        user = nil
      end
    end
    user
  end
end
Звичайно, цей метод написаний з метою демонстрації. Насправді, логіка аутентифікації може бути настільки складною, наскільки це необхідно.

Після успішної аутентифікації, екземпляр класу User серіалізується (Warden::Manager.serialize_into_session) у сесію. Далі його можна отримати через змінну оточення env['warden'].user, шляхом десеріалізації (Warden::Manager.serialize_from_session) по ідентифікатору.

Маршрути Sinatra

Ось ми і дісталися до завершальної стадії нашої статті. Настав час визначити маршрути для для нашого веб-додатку.
get '/' do
  # ...
end

get '/login/?' do
  # ...
end

post '/login/?' do
  # ...
end

get '/logout/?' do
  # ...
end

На головній сторінці виводитиметься привітання для користувача, якщо він автентифікований на сайті. Інакше переправлятиметься на сторінку '/login' з формою для аутентифікації.
get '/' do
  redirect '/login' unless env['warden'].user
  slim :index
end

get '/login/?' do
  slim :login
end

Головна сторінка:
p Welcome, #{env['warden'].user.username}
a href='/logout' Log out

Форма для аутентифікації містить для текстових поля 'username' і 'password'.
form action='/login' method='post'
  ul
    li#username
      label Username:
      br
      input name='username' type='text'
    li#password
      label Password:
      br
      input name='password' type='text'

  input type='submit' value='Log in'

Після того як користувач введе ім'я і пароль відбувається перевірка аутентифікації. І якщо вона пройшла успішно, користувач переправляється на головну сторінку з привітанням. Ця ж сторінка містить посиланням для виходу.
post '/login/?' do
  if env['warden'].authenticate
    redirect '/'
  else
    redirect '/login'
  end
end

get '/logout/?' do
  env['warden'].logout
  redirect '/'
end

На всяк випадок, викладу тут повний код додатку.

Немає коментарів: