середу, 26 жовтня 2011 р.

Модель User в DataMapper для Warden

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

За основу взята 11 глава "Task F: Administration" з книги Agile Web Development with Rails (3rd edition).

Тут основний упор буде на роботу з DataMapper. Крім того у другій частині статті ми закладемо фундамент адміністративного інтерфейсу для керування користувачами.


Замість того, щоб зберігати паролі у вигляді звичайного тексту, ми будемо пропускати їх через алгоритм SHA1, в результаті чого отримаємо 160-бітне хеш-значення. Перевіряти паролі ми будемо шляхом обробки отриманого від користувача пароля, порівнюючи його хешоване значення із значенням, що зберігається в базі даних. Ця система стане ще більш безпечною, шляхом "соління" (Salt) паролю, що змінює початкове число(при генерації псевдовипадкових чисел), яке використовуються при створенні хешу, поєднуючи пароль з псевдовипадковим рядком.

Додавання користувачів

Для початку нам необхідно змінити нашу модель User для користувачів. Складність полягає у тому, що вона повинна працювати з текстовою версією паролю(password), але значення "солі"(salt) і хешований пароль(hashed_password) зберігати у базі даних. Отже, наша модель User разом з перевірками матиме наступний вигляд:
class User
  include DataMapper::Resource

  property :id,              Serial
  property :name,            String
  property :hashed_password, String
  property :salt,            String

  timestamps :at

  attr_accessor :password_confirmation

  validates_presence_of     :name
  validates_uniqueness_of   :name
  validates_confirmation_of :password

  validates_with_method :password_non_blank

  private

  def password_non_blank
    if hashed_password.nil? || hashed_password.empty?
      [ false, 'Missing password']
    else
      true
    end
  end

end

Тут досить багато перевірок, як для такої простої моделі. Ми перевіряємо, що ім'я(name) присутнє і унікальне (тобто, два користувачі не можуть мати таке ж ім'я в базі даних).

Далі йде таємниче validates_confirmation_of. Вам, напевне, знайомі такі форми, у яких пропонується ввести пароль і його підтвердження, щоб переконатися, що ви не помилися. DataMapper може автоматично перевіряти чи два паролі збігаються. Ми побачимо, як це працює за хвилину. А зараз ми просто повинні знати, що нам потрібно два поля для паролів: одне для фактичного паролю(password), інше для його підтвердження(password_confirmation).

Нарешті, ми перевіряємо, чи пароль був встановлений. Але ми не перевіряємо атрибут password. Чому? Бо насправді його не існує, принаймні не в базі даних. Замість цього, ми перевіряємо наявність його посередника, хешованого паролю(hashed_password). Але щоб зрозуміти це, ми повинні подивитися на те, як ми звертаємося до паролів.

Перш за все, давайте подивимося, як створюється зашифрований пароль. Фокус в тому, щоб створити унікальне значення "солі"(salt), об'єднавши його з текстовим паролем(password) в один рядок, а потім запустити SHA1 дайджест. В результаті отримаємо 40-символьний рядок шістнадцяткових цифр.
Це все ми напишемо, як приватний метод класу:
def self.encrypted_password(password, salt)
  string_to_hash = password + "wibble" + salt
  Digest::SHA1.hexdigest(string_to_hash)
end

"Сіль"(salt) ми створимо шляхом об'єднання випадкового числа з ідентифікатором об'єкта user. Не має великого значення якої довжини "сіль", головне це її непередбачуваність (наприклад, використання часу, в якості "солі" має меншу ентропію ніж випадкові рядки). Ми зберігаємо цю нову "сіль" в атрибут salt моделі об'єкта. Так як це приватний метод, помістимо його після ключового слова privat :
private

def create_new_salt
  self.salt = self.object_id.to_s + rand.to_s
end

Тепер нам потрібно написати код так, щоб всякий раз як новий текстовий пароль(password) зберігається в об'єкті User, ми автоматично створюємо його хешовану версію(hashed_password), яка зберігатиметься в базі даних.
Зробимо текстовий пароль(password) віртуальним атрибутом моделі. Для нашого додатку він виглядатиме, як атрибут, але не зберігатиметься в базі даних.

Якби не було необхідності створювати хешований пароль, ми просто могли б написати:
attr_accessor :password
За лаштунками, attr_accessor генерує два методи: для читання password і для запису password=. Ми ж напишемо свої власні публічні методи:
def password
  @password
end

def password=(pwd)
  @password = pwd
  return if pwd.empty?
  create_new_salt
  self.hashed_password = User.encrypted_password(self.password, self.salt)
end

Нам залишилось лише, написати відкритий метод User#authenticate, який повертатиме екземпляр класу User, якщо передане коректне ім'я користувача і пароль. Оскільки вхідний пароль є у вигляді простого тексту, ми повинні знайти запис користувача, використовуючи ім'я(name) в якості ключа. А потім використати значення "солі"(salt) в цьому записі, щоб відтворити зашифрований пароль(expected_password). І повернути об'єкт user, якщо зашифровані паролі співпадають.
def self.authenticate(name, password)
  user = first(:name => name)
  if user
    expected_password = encrypted_password(password, user.salt)
    if user.hashed_password != expected_password
      user = nil
    end
  end
  user
end
Як ви пам'ятаєте з попередньої статті, Warden використовує цей метод для перевірки аутентифікації користувачів.

На всяк випадок, викладу повний код моделі перш ніж ми перейдемо до наступної частини статті.
class User
  include DataMapper::Resource

  property :id,              Serial
  property :name,            String
  property :hashed_password, String
  property :salt,            String

  timestamps :at

  attr_accessor :password_confirmation

  validates_presence_of     :name
  validates_uniqueness_of   :name
  validates_confirmation_of :password

  validates_with_method :password_non_blank

  def self.authenticate(name, password)
    user = first(:name => name)
    if user
      expected_password = encrypted_password(password, user.salt)
      if user.hashed_password != expected_password
        user = nil
      end
    end
    user
  end

  def password
    @password
  end

  def password=(pwd)
    @password = pwd
    return if pwd.empty?
    create_new_salt
    self.hashed_password = User.encrypted_password(self.password, self.salt)
  end

  private

  def password_non_blank
    if hashed_password.nil? || hashed_password.empty?
      [ false, 'Missing password']
    else
      true
    end
  end

  def create_new_salt
    self.salt = self.object_id.to_s + rand.to_s
  end

  def self.encrypted_password(password, salt)
    string_to_hash = password + "wibble" + salt
    Digest::SHA1.hexdigest(string_to_hash)
  end

end

Адміністрування користувачів

В Sinаtra визначимо маршрути для роботи з моделлю User, реалізувавши 7 стандартних методів: index, show, new, create, edit, update і delete.
# index
get '/users/?' do
  @users = User.all(:order => [ :name.asc ])

  slim :'/users/index'
end

# new
get '/users/new' do
  @user = User.new

  slim :'/users/new'
end

# show
get '/users/:id' do
  @user = User.first(params[:id])

  slim :'/users/show'
end

# edit
get '/users/:id/edit' do
  @user = User.first(params[:id])
end

# create
post '/users' do
  @user = User.create(:name => params[:name], :password => params[:password], :password_confirmation => params[:password_confirmation])

  if @user.save
    redirect '/users'
  else
    slim :'/users/new'
  end
end

# update
put '/users/:id' do
  @user = User.first(params[:id])

  if @user.update(:name => params[:name], :password => params[:password], :password_confirmation => params[:password_confirmation])
    redirect '/users'
  else
    slim :'/users/edit'
  end
end

# destroy
delete '/users/:id' do
  @user = User.first(params[:id])
  @user.destroy!

  redirect '/users'
end

І на сам кінець, представлення з формою для додавання нових користувачів.
new.slim
form method='post' action='/users'
  fieldset
    legend Enter User Details
    div
      label Name:
      input type='text' name='name'
    div
      label Password:
      input type='password' name='password'
    div
      label Confirm:
      input type='password' name='password_confirmation'

  input type='submit' value='Add User'

У цій статті я не буду зупинятися на інших представленнях.
Повний код додатку можна подивитись тут.

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