Authentication with Warden, devise-less
Bài đăng này đã không được cập nhật trong 8 năm
Warden là gì?
Đối với hầu hết chúng ta thì việc sử dụng warden
cũng có nghĩa là sử dụng devise
. Devise
thực sự rất tốt, nhưng nó quá lớn và có một những quy tắc bắt buộc chúng ta cần tuân theo. Vậy warden
là gì?
Warden là một Rack-based middleware, nó được thiết kế để cung cấp một cơ chế cho việc authentication trong các ứng dụng web Ruby. Nó là một cơ chế chung phù hợp với Rack Machinery để cung cấp một tùy chọn mạnh mẽ cho việc authentication.
Warden được thiết kế theo kiểu lười biếng. Nó có nghĩa là nếu bạn không sử dụng nó, nó sẽ không làm bất cứ điều gì, nhưng nếu bạn sử dụng nó sẽ tạo ra những hành động và cung cấp một cơ chế cho phép authentication trong bất kì một ứng dụng Rack-based nào.
Tại sao dùng Warden?
Với việc đẩy mạnh theo hướng sử dụng các Rack applications rất nhiều cơ hội được mở ra. Lời hứa về việc có nhiều ứng dụng cùng chạy trong một process, sub-applications và sub-sub-applications có thể được thực hiện.
Hiện nay việc tạo nhiều ứng dụng trong cùng một process đã và đang thu hút rất nhiều người. Vậy một câu hỏi được đặt ra là làm thế nào để quản lý authentication trong trường hợp này? Mỗi ứng dụng có thể yêu cầu một authentication hoặc một user
. Nhìn chung, authentication
có thể là cái gì đó giống như "user" cho tất cả các application trong cùng mảng lưới của Rack cho phép đăng nhập vào hệ thống.
Warden cho phép tất cả các middlewares và các endpoints chia sẽ chung một cơ chế authentication, đồng thời vẫn cho phép các ứng dụng tổng thể quản lý nó. Mỗi application có thể truy cập authentication user, hoặc là request authentication trong cùng một cách, sử dụng cùng logic trong cùng mạng lưới Rack.
Sử dụng như thế nào?
Warden
là tuyệt vời, nhưng mà nó không hề đơn giản để bắt đầu như đối với devise
. Mặt khác, toàn bộ quá trình authentication đều do bạn thực hiện, nó có nghĩa là bạn cần kiểm soát nhiều hơn và linh hoạt hơn khi nhu cầu sử dụng của bạn thay đổi.
Warden sử dụng Strategies để authenticate sessions. Tuy nhiên nó lại không cung cấp bất kì strategy nào bạn cần phải tự mình thực thi nó. Nếu sử dụng devise thì bạn đã biết, nó cung cấp một số strategy sau.
Devise đã thêm một số tính năng cũng như công cụ hữu ích, trước khi bạn đi sâu vào warden bạn cũng nên biết thêm một số tính năng sẽ không có khi bạn sử dụng warden như:
- Helpers giống như
current_user
,user_signed_in?
, etc. - Các điều hướng mặc định khi authentication fail hoặc là những request từ khách
- Tất cả các controllers. Đừng lo lắng, nó nghe có vẻ tệ hơn so với thực tế
- Những tùy chọn configuration mặc định
Bây giờ chúng ta sẽ cùng nhau implement warden. Ví dụ dưới sẽ thực thi authenticate với user trên web app lẫn API call. Tuy nhiên phần giới thiệu sẽ chỉ thực thi trong Web app, phần call API demo các bạn xem thêm trong phần demo (API chỉ implement cho việc authenticate chưa có phần đăng kí mới người dùng).
Đầu tiên, chúng ta cần phải add warden
vào trong Gemfile
gem "warden"
Chúng ta sẽ sử dụng model user
để thực hiện việc authenticate và model đó sẽ thực hiện những tính năng
- Mã hóa password
- Confirm sau khi người dùng đăng kí thành công => cần một file migration với thuộc tính sau:
rails g model User email:string encrypted_password:string confirmation_token:string confirmation_sent_at:datetime authentication_token:string
Chúng ta sẽ có 3 controllers cần thực thi, một là sessions để đăng nhập, registration để tạo mới người dùng và confirmation để confirm user. Chúng ta định nghĩ routes theo một cách đơn giản sau (nó phụ thuộc vào cách mà bạn muốn đặt)
resources :users do
collection do
resource :registrations, only: [:show, :new, :create]
resource :sessions, only: [:new, :create, :destroy]
resource :confirmations, only: [:show]
end
end
Và model User như sau:
require 'bcrypt'
class User < ActiveRecord::Base
before_create :generate_confirmation_token
validates_presence_of :email
include BCrypt
def password
@password ||= Password.new self.encrypted_password
end
def password=(new_password)
@password = Password.create(new_password)
self.encrypted_password = @password
end
def confirm!
self.confirmation_token = nil
self.confirmed_at = Time.now.utc
self.save!
end
private
def generate_confirmation_token
token = Digest::SHA1.hexdigest("#{Time.now}-#{self.id}-#{self.updated_at}")
self.confirmation_token = token
self.confirmation_sent_at = Time.now.utc
end
end
Sau đó cần add Warden
vào Rack Middleware
.
Rack là một kĩ thuật cho phép chúng ta kết nối nhiều ứng dụng trong cùng một server, có thể bạn không biết nhưng rails có sử dụng một số rack middleware, mỗi phần có nhiệm vụ, xử lý khác nhau. Để xem danh sách các rack middleware trong project bạn sử dụng ta gõ lệnh
rake middleware
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001f0dab0>
use Rack::Runtime
use Rack::MethodOverride
......
Một điều quan trọng là thứ tự xuất hiện của các rack middleware rất quan trọng nó ảnh hưởng đến việc các tính năng nào sẽ được sử dụng trong các rack tiếp theo (có thể tìm hiểu thêm về rack middleware tại đây). Vậy chúng ta nên đặt warden
ở đâu?
Nhìn vào wiki chúng ta có thể thấy rằng Warden must be downstream of some kind of session middleware.
Tuy nhiên bạn không nên add chúng sau session vì nếu chỉ add sau session vì flash sẽ không được dùng ở một số nơi và bạn sẽ không dùng được chúng. Vì vậy nên add sau ActionDispatch::Flash
.
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
end
Bây giờ cùng xem lại danh sách các middleware, bạn sẽ thấy use Warden::Manager
ngay sau use ActionDispatch::Flash
.
Sau đó chúng ta sẽ định nghĩ một Strategy
để authenticate user thông qua password(với web) và thông qua authentication_token(cho API) hoặc một phương thức authentication nào mà bạn định nghĩa.
Bạn cũng có thể xem thêm về Strategies tại wiki, nhưng khái quát thì nó sẽ thực thi các function valid!
, và authenticate!
. Bạn cũng có thể định nghĩa nhiều strategies khác nhau, mỗi strategy sẽ thực thi một authenticate cho mỗi app mà bạn định nghĩa, hoặc là hoàn toàn có thể dùng chung trong một strategy.
#lib/strategies/password_strategy.rb
class PasswordStrategy < ::Warden::Strategies::Base
def valid?
email || password || authentication_token
end
def authenticate!
if authentication_token
user = User.find_by_authentication_token authentication_token
user.nil? ? fail!("Could not log in") : success!(user)
else
user = User.find_by_email email
if user.nil? || user.confirmed_at.nil? || user.password != password
fail!("Could not log in")
else
success! user
end
end
end
private
def email
params["session"].try :[], "email"
end
def password
params["session"].try :[], "password"
end
def authentication_token
params["authentication_token"]
end
end
File này mình gộp authenticate cho web và API dùng chung một strategy. Giải thích qua một số tính năng của những function trên:
- Đầu tiên sẽ chạy qua hàm valid! nếu pass nó sẽ tiếp tục thực thi phần authenticate! Không sẽ tiếp tục check các strategy tiếp theo
- Trong hàm authenticate!, hàm
success!
chính là thực thi chức nănglogin
fail!
sẽ dừng lại tiến trình không check tiếp bất cứ strategy nào nữa- Còn một public function nữa là
fail
hàm này khácfail!
là sẽ không dừng lại tiến trình, mà tiếp tục check các strategy tiếp theo.
Đã implement xong strategy, bây giờ cần phải add chúng vào warden.
# config/application.rb
# Add Warden in the middleware stack
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
manager.default_strategies :password
# Nếu có thêm strategy thì bạn chỉ việc thêm tên strategy vào sau dòng trên, cách nhau bởi dấu ','
# ví dụ
# manager.default_strategies :password, :basic_auth
end
#config/initializers/warden.rb
require Rails.root.join('lib/strategies/password_strategy')
Warden::Strategies.add(:password_strategy, PasswordStrategy)
Giờ chúng ta sẽ implement một số helper tương tự devise
như là current_user
, signed_in?
và include vào ApplicationController
#app/controllers/concerns/warden_helper.rb
module WardenHelper
extend ActiveSupport::Concern
included do
helper_method :warden, :signed_in?, :current_user
prepend_before_action :authenticate!
end
def signed_in?
!current_user.nil?
end
def current_user
warden.user
end
def warden
env['warden']
end
def authenticate!
warden.authenticate!
end
end
Về cơ bản thì quá trình authenticate cho một API là xong, còn đối với Web chúng ta cần implement thêm 2 controller session cho việc login và registration để đăng kí.
#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
skip_before_filter :authenticate!
def create
authenticate!
redirect_to :root
end
def destroy
warden.logout
redirect_to :root
end
end
và
#app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
skip_before_action :authenticate!
def new
@user = User.new
end
def create
@user = User.new user_params
if @user.save
flash[:notice] = "Registrations User Success"
redirect_to root_path
end
end
private
def user_params
params[:user].permit!
end
end
Về cơ bản thì việc thực thi đăng kí đăng nhập cho Web đã xong. Chắc hẳn bạn sẽ có một thắc mắc khi nhìn vào hai controllers trên là không biết khi mà đăng nhập, hoặc đăng kí failed nó sẽ xử lý như thế nào?
Có thể thấy được devise
đã làm điều này rất tốt, vì nó đã xây dựng một failure_app cho phép xử lý tất cả các tình huống đăng nhập, đăng kí lỗi thì điều hướng người dùng tới đâu, trả về message gì ..... Thì ở đây chúng ta cũng xây dựng một Failure app tương tự như vậy tuy nhiên sẽ đơn giản hơn rất nhiều.
Thứ nhất chúng ta cần nói cho Warden biết chúng ta có một failure_app
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = UnauthorizedController
end
Tạo một class UnauthorizedController điều hướng những hoạt động khi mà đăng kí, đăng nhập failed thì sẽ như thế nào. Ví dụ, khi chưa đăng nhập, thì chuyển về trang đăng nhập trước khi người dùng sử dụng ứng dụng.
#app/controllers/unauthorized_controller.rb
class UnauthorizedController < ActionController::Metal
include ActionController::UrlFor
include ActionController::Redirecting
include Rails.application.routes.url_helpers
include Rails.application.routes.mounted_helpers
delegate :flash, to: :request
class << self
def call env
@respond ||= action :respond
@respond.call env
end
end
def respond
unless request.get?
message = env['warden.options'].fetch(:message, "unauthorized.user")
flash.alert = I18n.t(message)
end
redirect_to new_sessions_url
end
end
Trên đây đã hoàn tất quá trình đăng kí, đăng nhập với warden, nó nhẹ nhàng và đơn giản hơn rất nhiều.
Demo
Một demo nhỏ về việc sử dụng warden cho việc authentication với web app và api, một cách authentication cho cả web và api. Các bạn có thể xem nó tại đây
References
http://pothibo.com/2013/07/authentication-with-warden-devise-less/ https://github.com/hassox/warden/wiki/Overview
All rights reserved