Але очевидно, з точки зору безпеки паролі у базі даних повинні зберігатися в зашифрованому вигляді. Цю тему ми і розглянемо у цій статті.
За основу взята 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'
У цій статті я не буду зупинятися на інших представленнях.
Повний код додатку можна подивитись тут.
Немає коментарів:
Дописати коментар