How Devise obtains the current_user
Bài đăng này đã không được cập nhật trong 3 năm
Lời nói đầu
Gem Devise chắc hẳn không còn xa lạ gì với RoR developers, là một gem rất mạnh và linh hoạt trong vai trò quản lí và xác thực người dùng. Ngoài 10 module chính thì Devise còn hỗ trợ developer các method helper khá hữu dụng như authenticate_user! , user_signed_in?, user_session, current_user.... Trong đó hàm current_user
được sử dụng khá nhiều, và bài viết này mình muốn đi sâu 1 chút xem current_user
của Devise cụ thể là hàm đó làm những gì, có hoạt động giống như ta nghĩ thông thường hay không.
How current_user Devise works
Trước tiên chúng ta cần build 1 app nhỏ với gem Devise, chi tiết có thể tham khảo tại: https://viblo.asia/p/gioi-thieu-gem-devise-amoG84YnGz8P
Bước đầu tiên đã xong. Để bắt đầu đi sâu vào tìm hiểu, ta hãy cùng nhìn vào source code của nó, cụ thể là trong đường dẫn: https://github.com/plataformatec/devise/blob/e33d285e4ae2130fe9acd00ef60782ae8bee6ad9/lib/devise/controllers/helpers.rb
Có thể thấy trong module Helpers, xuất hiện method có dạng:
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
Lưu ý các Authentication Filters và Accessor Helpers trong Devise đều được định nghĩa dựa trên "mapping", vì vậy method trên cũng tương ứng với:
def current_user
@current_user ||= warden.authenticate(scope: :user)
end
Trong trường hợp @current_user
nil hoặc false, sẽ được gán bằng warden.authenticate(scope: :user)
, là authentication-method của Warden (một rack based middleware, cung cấp cơ chế cho việc authenticate trên các ứng dụng web ruby), method này sẽ thực hiện hành động xác thực với params truyền vào là user được biểu diễn dưới dạng scope . Lí do cho việc này là Devise sử dụng nhiều scopes của Warden (chẳng hạn như admin và user).
warden.authenticate(scope: :user)
cũng chấp nhận các tham số "strategy". Warden sử dụng strategy như là một nơi để lưu trữ logic cho việc authenticate 1 request. Trong trường hợp này, không có strategy params nào được truyền vào nên Warden sẽ sử dụng "default strategy" :
class DatabaseAuthenticatable < Authenticatable
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
hashed = false
if validate(resource){ hashed = true; resource.valid_password?(password) }
remember_me(resource)
resource.after_database_authentication
success!(resource)
end
mapping.to.new.password = password if !hashed && Devise.paranoid
fail(:not_found_in_database) unless resource
end
end
Strategy này được dùng cho việc sign in user. Ở đây bản thân user chính là 1 resource, sau quá trình authenticate và validate, method success!(resource)
được thực thi. Method này là 1 trong 6 actions chính được sử dụng trong 1 strategy.
Method success!
được gọi cùng với 1 user object để cho phép user log in. Ngay sau đó Warden sẽ khởi tạo và lưu user vào Session Hash, bằng cách "serialize" user thông qua env["rack.session"]
. Để kiểm chứng xem cụ thể session ở đây lưu những gì, ta tiếp tục vào source code của class SessionSerialize của Warden để tham khảo: https://github.com/hassox/warden/blob/906edf86c6c31be917a921097031b89361d022e8/lib/warden/session_serializer.rb
Ta thử nhìn vào 2 hàm là store
và fetch
:
def store(user, scope)
return unless user
method_name = "#{scope}_serialize"
specialized = respond_to?(method_name)
session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user)
end
def fetch(scope)
key = session[key_for(scope)]
return nil unless key
method_name = "#{scope}_deserialize"
user = respond_to?(method_name) ? send(method_name, key) : deserialize(key)
delete(scope) unless user
user
end
Trong hàm Store(scope)
, ta thấy dòng code:
session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user)
Như vậy ở đây session đã được khởi tạo và store data với key key_for(scope)
.
Tiếp tục mò mẫm trong class SessionSerialize, ta thấy method key_for(scope)
được định nghĩa:
def key_for(scope)
"warden.user.#{scope}.key"
end
Liệu#{scope}
ở đây có phải biểu diễn cho user? Chúng ta thử đặt byebug và tiến hành loggin xem:
Trước tiên chúng ta cần generate ra SessionsController:
rails g devise:controllers users
Đừng quên khai báo lại trong routes:
devise_for :users, controllers: {sessions: 'users/sessions'}
Sau đó đặt byebug trong action create của class Users::SessionsController
:
def create
super
byebug
end
Loggin xong, ta vào terminal, kiểm tra current_user:
Tiếp tục kiểm tra thử session["warden.user.user.key"]
. Kết quả thu được:
Vậy là ở đây session lưu lại id của user hiện tại, và kèm thêm chuỗi token.
Tài liệu tham khảo
Bài viết của mình được tham khảo từ nhiều nguồn, các bạn có thể tìm hiểu thêm tại: https://github.com/plataformatec/devise https://github.com/hassox/warden/wiki http://kadwill.com/tag/authentication/ https://insights.kyan.com/devise-authentication-strategies-a1a6b4e2b891 http://www.rubydoc.info/github/plataformatec/devise/Devise
All rights reserved