Authentincation với JWT trong Rails 5
Bài đăng này đã không được cập nhật trong 7 năm
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
Giới thiệu về Authentication
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.
Sử dụng JWT (JSON WEB TOKEN)
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
Xử lý API request
Ở đâ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
Tổng kết
Đế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
All rights reserved