Велика кількість аплікацій потребує систему реєстрації та автентифікації користувачів. Це складне завдання. Так як йдеться про безпеку персональних даних. На щастя існує декілька хороших рішень для Rails фреймворку. Мабуть, найпоширенішим є Devise. Devise є досить простим у встановленні. Складність полягає у великій кількості можливих сценаріїв.
В залежності від потреб бізнесу можна організувати різні способи реєстрації. Якщо достаньо отримати пошту користувача, яка буде також виконувати роль логіну, і запитувати пару логін / пароль для входу, то стандартні налаштування Devise - це те що потрібно.
У цій статті ми розглянемо більш складний flow:
- Відвідувач має можливість обрати спосіб реєстрації: за допомогою аккаунтів facebook чи google, або через пошту.
- Після реєстрації через пошту користувач може під'єднати аккаунти соціальних мереж (facebook та/чи google)
- Після входу через один із аккаунтів соціальних мереж користувач отримує лист де має змогу перейти на сторінку встановлення паролю для входу.
- В подальшому користувач може від'єднати акаунти соціальних мереж та повністю видалити акаунт на сайті.
У статті ми крок за кроком пройдемо весь шлях реалізації даного завдання.
Посилання на репозиторій https://github.com/RubyForceTeam/authentication_solution_illia108.git
Створюємо нову rails аплікацію з базою даних PostgreSQL (у разі розгортання на heroku це полегшить роботу):
rails new auth --database=postgresql
cd auth
Додаємо необхідні бібліотеки:
Gemfile
gem 'devise'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem 'dotenv-rails', groups: [:development, :test]
Перші три gem'и потрібні безпосередньо для реєстрації та авторизації користувачів. Останній dotenv-rails
- для збереження конфіденційної інформації. Ми повернемось до нього пізніше.
bundle
Створюємо базу даних:
rails db:create
Налаштовуємо Devise:
rails generate devise:install
rails generate devise User
rake db:migrate
В результаті отримуємо модель User
, відповідну міграцію та шлях.
Devise потребує визначеної головної сторінки:
rails g controller home index
Згенерували новий контролер із методом index
. Та встановлюємо цей метод на root_path
:
config/routes.rb
Rails.application.routes.draw do
root to: 'home#index'
devise_for :users
end
Для зручності додамо мінімальну навігацію:
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Auth</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<%= link_to 'Home', root_path %>
<%= link_to 'Users', users_path %>
<% if current_user %>
<%= link_to('Logout', destroy_user_session_path, method: :delete) %>
<% else %>
<%= link_to('Login', new_user_session_path) %>
<% end %>
<%= yield %>
</body>
</html>
Стартуємо сервер та перевіряємо результат:
rails s
Першу частину завершено. У нас є реєстрація користувачів за допомогою логіну та паролю.
Ми одразу ж розділимо дані користувача та способи автентифікації. Для цього створимо нову модель Authentication
:
rails g model authentication provider:string uid:string user_id:integer
app/models/authentication.rb
class Authentication < AplicationRecord
belongs_to :user
end
Згенеруємо контролер:
rails g controller authentications
app/controllers/authentication_controller.rb
class AuthenticationsController < ApplicationController
def destroy
@authentication = current_user.authentications.find(params[:id])
@authentication.destroy
flash[:notice] = "Successfully destroyed authentication."
redirect_to root_path
end
end
Єдина дія яку ми будемо виконувати безпосередньо над автентифікацією - це видалення. Створення буде відбуватися у іншому контролері відповідальному за callback'и.
В модель User
додаємо зв'язок з authentications
. А також додаткові модуль omniauthable
та провайдерів. Метод apply_omniauth
будує відповідні authentications
для корстувача. А метод класу set_user
повертає існуючого або створює нового користувача. Обидва методи ми використаємо у callbacks_controllers.rb
.
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
has_many :authentications, dependent: :destroy
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable, omniauth_providers: [:facebook, :google_oauth2]
def apply_omniauth(omniauth)
authentications.build(provider: omniauth['provider'], uid: omniauth['uid'])
end
def self.set_user(omniauth)
User.find_by(email: omniauth.info.email) || User.new(email: omniauth.info.email, password: Devise.friendly_token[0,20])
end
end
Створюємо таблицю authentications
:
rake db:migrate
Наступний крок - створити аплікації на Facebook та Google
Для цього реєструємось на developers.facebook.com
та console.developers.google.com
. Кожна аплікація надає нам ID та SECRET. Також важливо вказати Valid OAuth redirect URIs
- це адреса нашого сайту http://localhost:3000/
.
Зауваження: якщо ви плануєте розгортати свою аплікацію локально (так як в даному прикладі) то деактивуйте Use Strict Mode for Redirect URIs
для Facebook.
Додаємо ID та SECRET:
config/initializers/devise.rb
...
# Facebook
config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET']
# Google
config.omniauth :google_oauth2, ENV['GOOGLE_ID'], ENV['GOOGLE_SECRET']
...
Ключі зберігаємо за допомого dotenv:
.env
...
FACEBOOK_ID=ХХХХХХХХХХХХХХХХ
FACEBOOK_SECRET=ХХХХХХХХХХХХХХХХ
GOOGLE_ID=ХХХХХХХХХХХХХХХХ
GOOGLE_SECRET=ХХХХХХХХХХХХХХХХ
...
Тепер потрібно створити контролер, який буде обробляти callback'и після авторизації. Для кожного провайдера потрібен окремий метод. Важлива саме назва. А так як функціонал методів однаковий, то можна використати alias_method
:
app/controllers/callbacks_controller.rb
class CallbacksController < Devise::OmniauthCallbacksController
def all
omniauth = request.env["omniauth.auth"]
authentication = Authentication.find_by_provider_and_uid(omniauth['provider'], omniauth['uid'])
if authentication
flash[:notice] = "Signed in successfully."
sign_in_and_redirect(:user, authentication.user)
elsif current_user || User.exists?(email: omniauth.info.email)
user = current_user || User.find_by_email(omniauth.info.email)
user.authentications.create!(:provider => omniauth['provider'], :uid => omniauth['uid'])
flash[:notice] = "Authentication successful."
if current_user
redirect_to root_path
else
sign_in_and_redirect(:user, user)
end
else
user = User.set_user(omniauth)
user.apply_omniauth(omniauth)
if user.save
flash[:notice] = "Signed in successfully."
send_set_password_email(user)
sign_in_and_redirect(:user, user)
else
cookies[:omniauth] = omniauth.except('extra')
redirect_to new_user_registration_url
end
end
end
alias_method :facebook, :all
alias_method :google_oauth2, :all
private
def send_set_password_email(user)
user.send_reset_password_instructions
end
end
Фактично, у цьому контролері міститься вся логіка нашої аплікації. Ми переверіяємо наявність автентифікацій та користувача і виконуємо відповідні дії по їх створенню та логінізації. Важливий момент - це надсилання листа новоствореному користувачу за допомогою send_set_password_email
. В даному випадку ми використовуємо вже існуючий mailer
Devise
'у reset_password_instructions
. Але можна також створити та налаштувати власний mailer
.
І додаємо шляхи для даного контролера, а також для users
та authentications
:
config/routes.rb
Rails.application.routes.draw do
root to: 'home#index'
devise_for :users, controllers: { omniauth_callbacks: 'callbacks' }
resources :users
resources :authentications, only: [:destroy]
end
Налаштування для роботи mailer
'а:
config/environment/development.rb
...
config.action_mailer.perform_caching = false
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_options = { from: ENV['EMAIL_USERNME'] }
config.action_mailer.default_url_options = { host: "localhost:3000" }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
port: 587,
domain: 'localhost',
user_name: ENV['EMAIL_USERNAME'],
password: ENV['EMAIL_PSWD'],
authentication: 'plain',
enable_starttls_auto: true }
...
Тут доволі стандартні налаштування для gmail
'a. Знову ж таки ми використовуємо змінні оточення (ENV) для вразливих даних (адреса пошти та пароль).
.env
EMAIL_USERNAME=ХХХХХХХХХХХХХХХХ
EMAIL_PSWD=ХХХХХХХХХХХХХХХХ
На даний момент є одна проблема. Коли користувач перейде за посиланням в листі, то відбудеться редірект на головну сторінку. Це відбувається тому що користувач залогінений. Для того щоб пропустити перевірку, потрібно модифікувати passwords_controller.rb
. Це один із контролерів Devise
'у, в якому міститься відповідна логіка. Створюємо контролер:
app/controllers/passwords_controller.rb
class PasswordsController < Devise::PasswordsController
skip_before_action :require_no_authentication, :only => [:edit, :update]
def update
super
if resource.errors.empty?
sign_out(resource_name)
sign_in(resource_name, resource)
end
end
end
Тепер при реєстрації за допомогою аккакнту соціальних мереж, користувач отримає лист із можливістю встановити новий пароль, який можна буде використовувати для логіну.
P.S.
На головній сторінці відображається повідомлення про те, що користувач з певним email
залогінився у певний спосіб:
app/views/home/index.html.erb
<p id="notice"><%= notice %></p>
<h1>Authentication with multiple social network accounts</h1>
<% if current_user %>
<p>You are logged in as <mark><%= current_user.email %></mark> with <mark><%= @provider %></mark></p>
<% else %>
<p>Please Log In</p>
<% end %>
У контролері визначена змінна @provider
, яка може містити назву провайдера (facebook
, google_oauth2
) або email
. Значення зберігається у cookies
:
app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
@provider = cookies[:provider]
end
end
Для того щоб мати коректне значення cookies[:provider]
створюємо ініціалізатор, який буде спрацьовувати після логінізації та після logout
'у користувача:
config/initializers/warden_hooks.rb
Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
if auth.request.env['omniauth.auth']
auth.cookies[:provider] = auth.request.env['omniauth.auth'][:provider]
else
auth.cookies[:provider] = 'email'
end
end
Warden::Manager.before_logout do |user,auth,opts|
auth.cookies.delete :provider
end
"Велика кількість аплікацій потребують систему..." - можливо краще замінити "потребують" на "потребує";
"Нащастя існує декілька.." - на щастя;
"...перейти на сторінку встановлення пароль для..." - паролю;
"В резултаті отримуємо" - результаті;
"створює новго користувача" - нового
"Кожна аплікаціє надає нам" - аплікація
"Згенерували новий контроллер із методом" - контролер