+4

How Devise obtains the current_user

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à storefetch:

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí