Authentincation với JWT trong Rails 5
Tiếp nối bài trước mình đã nói về tạo api trong rails 5 thì trong bài này mình sẽ tiếp tục chia sẽ thên về authentication bằng JWT và 1 số tính năng khác về api trong rails 5 như API versioning , pagination , và serialization Như chúng ta đã biết thì API sẽ cũng cấp cho người dùng có ...
Tiếp nối bài trước mình đã nói về tạo api trong rails 5 thì trong bài này mình sẽ tiếp tục chia sẽ thên về authentication bằng JWT và 1 số tính năng khác về api trong rails 5 như API versioning, pagination, và serialization
Như chúng ta đã biết thì API sẽ cũng cấp cho người dùng có thể có thao tác những hoạt động riêng của họ, quản lý các hoạt động đó của họ.
Đầu tiên chúng ta sẽ tạo ra User, vì tất nhiên phải có người dùng thì chúng ta mới cần tới việc authentication.
rails g model User name:string email:string password_digest:string rails db:migrate
Nói 1 chút về Molde User này, tại sao chúng ta lại dùng password_disget thay vì dùng password vì chúng ta sẽ sử dụng 1 method là has_secure_password để thực hiện xác thực một bcrypt password, điều đó đòi hỏi chúng ta phải sử dụng password_disget attribute.
Users sẽ thực hiện quản lý nhiều Todo lists của họ, chính vì thế chúng ta sẽ có một mối quan hệ 1 - many ở đây giữa modle User và model Todo (như ở phần trước mình đã trình bày).
class User < ApplicationRecord has_secure_password has_many :todos, foreign_key: :created_by validates_presence_of :name, :email, :password_digest end
để thực hiên bcrypt password chúng ta cần sử dụng gem bcrypt.
sau khi thưc hiện :
bundle install
để sử dụng gem. Bây giờ chúng ta sẽ tiến hành đi vào authentication, trước tiên cần định nghĩa các class service chúng ta cần sử dụng.
- JsonWebToken : sử dụng để encode và decode bằng việc sử dung jwt
- AuthorizeApiRequest: sử dụng để xác thực mỗi request API.
- AuthenticateUser : sử dụng để xác thực User.
- AuthenticationController: sử dụng để thực hiện xác thực lúc User login vào hệ thống.
Chúng ta sẽ sử dụng gem jwt. Sau khi thực hiện cài và bundle gem jwt, chúng ta sẽ tạo thư mục lib bên trong app, vì sao lại là app/lib, bởi vì tất cả code trong app đều được auto-loaded trong môi trường development và eager-loaded trong môi trường production.
# app/lib/json_web_token.rb class JsonWebToken HMAC_SECRET = Rails.application.secrets.secret_key_base def self.encode(payload, exp = 24.hours.from_now) payload[:exp] = exp.to_i JWT.encode(payload, HMAC_SECRET) end def self.decode(token) body = JWT.decode(token, HMAC_SECRET)[0] HashWithIndifferentAccess.new body rescue JWT::ExpiredSignature, JWT::VerificationError => e raise ExceptionHandler::ExpiredSignature, e.message end end
Trong class trên chúng ta thấy JWT sử dụng 2 method là encode và decode.
- Method encode sẽ tạo ra token dựa trên payload là user_id và thời gian hết hạn (expiration). lúc đó ta sẽ có token để thực hiện ứng dụng như một key xác thực.
- Method decode sử dụng để decode token bằng việc sử dụng HMAC_SECRET một đoạn mà để encode trước đó. Trong quá trình decode nếu xảy ra lỗi như hết hạn hoặc validation, JWT sẽ raised những exception, lúc này chúng ra sẽ xử lý bằng module ExceptionHandler.
module ExceptionHandler extend ActiveSupport::Concern class AuthenticationError < StandardError; end class MissingToken < StandardError; end class InvalidToken < StandardError; end included do rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two rescue_from ActiveRecord::RecordNotFound do |e| json_response({ message: e.message }, :not_found) end rescue_from ActiveRecord::RecordInvalid do |e| json_response({ message: e.message }, :unprocessable_entity) end end private def four_twenty_two(e) json_response({ message: e.message }, :unprocessable_entity) end def unauthorized_request(e) json_response({ message: e.message }, :unauthorized) end end
Ở đây chúng ta sẽ xử lý tất cả các API request, để đảm bảo răng tất cả các request đều được valid token. chúng ta sẽ tạo thưc mục auth trong app.
class AuthorizeApiRequest def initialize(headers = {}) @headers = headers end def call { user: user } end private attr_reader :headers def user @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token rescue ActiveRecord::RecordNotFound => e raise( ExceptionHandler::InvalidToken, ("#{Message.invalid_token} #{e.message}") ) end def decoded_auth_token @decoded_auth_token ||= JsonWebToken.decode(http_auth_header) end def http_auth_header if headers['Authorization'].present? return headers['Authorization'].split(' ').last end raise(ExceptionHandler::MissingToken, Message.missing_token) end end
Service này sẽ có 1 method là call sẽ trả về một user object khi thực hiện một request thành công. Nó get token từ authorization headers, và thực hiện decode để trả về 1 user object. trong trường hợp xảy ra lỗi thì chúng ta cần care điều đó bằng cách sẽ trả về code lỗi và message thông báo. Vì thế chúng ta sẽ tạo thêm 1 service Message để quản lý Message của app. Chúng ta sẽ vứt nó vào trong app/lib.
class Message def self.not_found(record = 'record') "Sorry, #{record} not found." end def self.invalid_credentials 'Invalid credentials' end def self.invalid_token 'Invalid token' end def self.missing_token 'Missing token' end def self.unauthorized 'Unauthorized request' end def self.account_created 'Account created successfully' end def self.account_not_created 'Account could not be created' end def self.expired_token 'Sorry, your token has expired. Please login to continue.' end end
Xong 2 thằng JsonWebToken : sử dụng để encode và decode bằng việc sử dung jwt , AuthorizeApiRequest: sử dụng để xác thực mỗi request API. Giờ chúng ta sẽ đi tiếp 1 thằng nữa là AuthenticateUser : Class này sẽ thực hiện xác thực User từ việc Login của user bằng email và pasword.
class AuthenticateUser def initialize(email, password) @email = email @password = password end def call JsonWebToken.encode(user_id: user.id) if user end private attr_reader :email, :password def user user = User.find_by(email: email) return user if user && user.authenticate(password) raise(ExceptionHandler::AuthenticationError, Message.invalid_credentials) end end
Giải thích 1 chút về class này, nó có 1 method call sẽ thực hiện trả về 1 token khi user được xác thực thành công bằng việc kiểm trả email và pasword của user.
Tiếp theo, chúng ta sẽ thực hiện việc tạo AuthenticationController để thực hiện các service Authentication đã tạo. Controller này cũng sẽ là endpoint của việc login trong app.
class AuthenticationController < ApplicationController def authenticate auth_token = AuthenticateUser.new(auth_params[:email], auth_params[:password]).call hash_authen = { status: true, data: { token: auth_token, name: "cuongdv" } } json_response(hash_authen) end private def auth_params params.permit(:email, :password) end end
trong config/routes.rb chúng ta sẽ config để no sẽ là login.
Rails.application.routes.draw do # [...] post 'auth/login', to: 'authentication#authenticate' end
nhưng trước tiên chúng ta cần tạo User thì mới cáo cái để mà Login. =))
class UsersController < ApplicationController def create user = User.create!(user_params) auth_token = AuthenticateUser.new(user.email, user.password).call response = { message: Message.account_created, auth_token: auth_token } json_response(response, :created) end private def user_params params.permit( :name, :email, :password, :password_confirmation ) end end
và routes lúc này thì sẽ có thêm thế này :
Rails.application.routes.draw do # [...] post 'signup', to: 'users#create' end
UserController sẽ tạo ra user và trả về JSON. chúng ta sử dung create! để khi có 1 error hoặc exception thì sẽ raised để xử lý.
Tới thì có thể đã xong những thứ cần thiêt, bây giờ chúng ta sẽ làm thế nào để mỗi request hay còn gọi là action trong rails, sẽ luôn được check authorize. Rails đã cung cấp before_action callback để giúp chúng ta thực hiện điều này, và chúng ta sẽ làm điều đó ở trong applicationController.
class ApplicationController < ActionController::API include Response include ExceptionHandler include ActionController::Serialization before_action :authorize_request attr_reader :current_user private def authorize_request @current_user = (AuthorizeApiRequest.new(request.headers).call)[:user] end end
khi mỗi request sẽ được xác thực bởi method authorize_request , nếu request xác thực thành công thì nó sẽ trả về 1 object curent_user để thực hiện ở controller khác. Nhưng ko phải request nào chúng ta cũng thực hiện xác thực, chẳng hạn như sign up, hay login thì chúng ta ko cần đến token. vì vậy chúng ta sẽ bỏ qua nó đối với những request kiểu như thế, nhờ vào callback skip_before_action trong rails.
class AuthenticationController < ApplicationController skip_before_action :authorize_request, only: :authenticate #[...] end
đối với request sign up của user controller:
class UsersController < ApplicationController skip_before_action :authorize_request, only: :create #[...] end
Đến đây thì có lẽ nó đã giúp bạn hiểu 1 phần nào đó về cách authentication bằng gem JWT trong API Rails 5. để tránh bị loãng về kiến thức mình tìm hiểu về JWT muốn chia sẽ, phần tiếp theo mình sẽ nói về các chức năng khác trong API Rails như API versioning, pagination, và serialization.
các bạn có thể tham khảo ở repo này của mình: https://github.com/duongvancuong/API_rails