12/08/2018, 14:40

Authenticate Your Rails API with JWT from Scratch

Xác thực là một trong những phần quan trọng của bất kỳ ứng dụng web. Có vô số các thư viện và các frameworks mà cung cấp tùy chọn khác nhau để thực hiện xác thực theo cách này hay cách khác. Các thư viện này lấy đi rất nhiều groundwork cần thiết để thiết lập một hệ thống xác thực, cung cấp "magic" ...

Xác thực là một trong những phần quan trọng của bất kỳ ứng dụng web. Có vô số các thư viện và các frameworks mà cung cấp tùy chọn khác nhau để thực hiện xác thực theo cách này hay cách khác. Các thư viện này lấy đi rất nhiều groundwork cần thiết để thiết lập một hệ thống xác thực, cung cấp "magic" với những gì đang xảy ra đằng sau hậu trường. Đối với Rails, chúng tôi có một số hệ thống xác thực, nổi bật là Devise.

Devise là một công cụ xác thực mà chạy như là một phần của ứng dụng của chúng tôi và làm tất cả các việc khi nói đến xác thực. Tuy nhiên, thường thì chúng ta không cần nhiều những phần nó cung cấp. Ví dụ, Devise không làm việc rất tốt với các hệ thống dựa trên API, đó là lý do tại sao chúng tôi có gem devisetokenauth. devisetokenauth là một thư viện mà những gì Devise không có, nhưng với tokens thay vì của sessions.

Hôm nay chúng ta sẽ đi tìm hiểu xây dựng hệ thống xác thực dựa trên JWT từ đầu.

Chú ý: Hướng dẫn này nhằm mục đích cho việc xác thực dựa trên API.

1. Tại sao là JWT

JWT (JSON Web Token, pronounced “jot”) là một tiêu chuẩn xác thực khép kín được thiết kế để trao đổi dữ liệu một cách an toàn giữa các hệ thống. Kể từ khi nó khép kín, nó không cần backing storage để làm việc. Ngoài ra, cách tiếp cận JWT là rất đáng tin cậy và linh hoạt, cho phép nó được sử dụng với bất kỳ khách hàng. Nó không có bất kỳ chi phí để bắt đầu và gần như tất cả các ngôn ngữ đều có thư viện mà làm cho làm việc với JWTs một dễ dàng.

Nếu bạn muốn tìm hiểu thêm về JWT hãy theo dõi bài viết tuyệt vời về JWT và làm thế nào để sử dụng nó với Rails. Nếu bạn là người mới biết đến JWT tôi muốn đề nghị bạn đọc nó đầu tiên để có được một ý tưởng về những gì chúng ta sẽ tạo ra.

2. model

Chúng tôi sẽ bắt đầu bằng việc xây dựng một model cho các ứng dụng của mình. Các ứng dụng sẽ xác thực các người dùng, vì vậy hãy tạo một model User.

rails g model user

Đây là lệnh tạo một file migration db/migrate được đặt tên XXXcreateusers, ở đây XXX là ngày hiện tại. Bạn cần tạo model User có các column như file sau:

create_table :users do |t|
  t.string :email,              null: false
  t.string :password_digest,    null: false
  t.string   :confirmation_token
  t.datetime :confirmed_at
  t.datetime :confirmation_sent_at

  t.timestamps
end

Sau đó bạn cần chạy rake db:migrate để chạy file migration vừa tạo.

Chúng ta cần thêm colunm email và password_digest ở đây, chúng là các cột cơ bản cần thiết để xác thực một user. Tất nhiên bạn có thể thêm các cột khác tùy vào ý muốn của mình. Các cột confirmation_token, confirmed_at và confirmation_sent_at cần yêu cầu user xác nhận. Bạn có thể bỏ qua điều này nếu bạn không muốn để xác nhận email.

3. Validations

Hãy thêm validations cho models/user.rb:

validates :email, presence: true, uniqueness: {case_sensitive: false}
validates :email, format: { with: /@/ }

Chúng ta validate uniqueness của email với trường hợp sensitivity và làm một kiểm tra email đơn giản đảm bảo rằng string email có chứa ký tự @. Điều này có thể không phải là một solid validation, nhưng không có một tiêu chuẩn và đó là lý do tại sao chúng tôi xác minh các email sau khi đăng ký.

4. Callbacks

Chúng tôi sẽ sử dụng các hassecurepassword của Rails để xử lý password hashing.

Để bắt đầu, cần thêm gem bcrypt vào Gemfile của bạn và sau đó chạy bundle install. Sau khi gem được cài đặt thì thêm dòng dưới đây vào class User trong file models/user.rb

class User < ApplicationRecord
  has_secure_password
  ...

Hãy thêm callbacks mà chúng ta muốn thực hiện trước khi tạo user. Đầu tiên chúng ta muốn lowercase các email và loại bỏ hết các space. Thứ hai chúng ta muốn sinh một token xác nhận mà sẽ được gửi trong email đến user.

Thêm các callbacks before vào file models/user.rb:

before_save :downcase_email
before_create :generate_confirmation_instructions

Chúng tôi downcase các email trước khi lưu nó vào DB. Các hướng dẫn xác nhận sẽ được tạo ra chỉ trong việc tạo ra các bản ghi user. Hãy thêm các method:

def downcase_email
  self.email = self.email.delete(' ').downcase
end

def generate_confirmation_instructions
  self.confirmation_token = SecureRandom.hex(10)
  self.confirmation_sent_at = Time.now.utc
end

Nếu bạn định bỏ qua xác nhận, bạn có thể bỏ qua các method trên và gọi lại tương ứng.

5. Registration

5.1. Create

Bây giờ chúng ta cài đặt model, nên hãy thêm một endpoint cho user được tạo. Tạo controller cho users:

rails g controller users

Thêm dòng dưới vào file config/routes.rb:

resources :users, only: :create

Bây giờ chúng ta đã tạo ra UsersController củamình. Hãy chuyển qua controller (app/controllers/users_controller.rb) và thêm những dòng sau đây:

def create
  user = User.new user_params

  if user.save
    render json: {status: "User created successfully"}, status: :created
  else
    render json: { errors: user.errors.full_messages }, status: :bad_request
  end
end

private

def user_params
  params.require(:user).permit :email, :password, :password_confirmation
end

Chúng ta có một API endpoint để tạo một user. Bạn có thể thử nó bằng cách start server và gửi yêu cầu POST với tiêu đề user như JSON trong nội dung. Ví dụ bạn có thể post đến http://localhost:3000/users

{
  user: {
    email: "test@example.com",
    password: "anewpassword",
    password_confirmation: "anewpassword"
  }
}

Bạn sẽ nhận được message User được tạo thành công như tương ứng. Các request tiếp theo với giống dữ liệu nên tương ứng với message error. Bạn có thể bỏ qua những điều sau này và đi vào phần Login.

5.2. Confirmation

Một trong những điều đang chờ xử lý là confirmation user. Chúng tôi sẽ gửi email xác nhận cho user trướckhi được tạo và tạo ra một endpoint mà validates token để xác nhận user.

Để bắt đầu, Chúng ta phải gửi một email tới user khi bản ghi được tạo thành công. Chúng ta sẽ không nói đến làm thế nào để gửi email ở đây. Chúng ta nên thêm dòng gửi email sau khi user.save trong users_controller.

...
if user.save
  #Invoke send email method here
...

Chỉ cần chắc chắn rằng bạn bao gồm user.confirmation_token trong email của bạn. Lý tưởng nhất , URL sẽ dẫn đến một endpoint mà tìm nạp token và gửi nó đến API của chúng tôi. Hãy tạo ra mà post API endpoint.

Thêm một route vào file config/routes.rb để xác nhận endpoint:

resources :users, only: :create do
  collection do
    post 'confirm'
  end
end

Bây giờ, tạo action xác nhận trong UsersController:

def confirm
  token = params[:token].to_s

  user = User.find_by(confirmation_token: token)

  if user.present? && user.confirmation_token_valid?
    user.mark_as_confirmed!
    render json: {status: 'User confirmed successfully'}, status: :ok
  else
    render json: {status: 'Invalid token'}, status: :not_found
  end
end

Hãy xem những gì chúng tôi đang làm ở đây. Đầu tiên, chúng ta lấy các token từ params, gọi to_s để xử lý các trường hợp token không được gửi trong request. Tiếp theo, lấy user tương ứng dựa trên các xác nhận token.

Nếu user tồn tại và confirmation chưa hết hạn, gọi phương thức model mark_as_confirmed! và tương ứng với message thành công. Chúng ta phải thêm các phương thức confirmation_token_valid? và mark_as_confirmed! vào model User:

def confirmation_token_valid?
  (self.confirmation_sent_at + 30.days) > Time.now.utc
end

def mark_as_confirmed!
  self.confirmation_token = nil
  self.confirmed_at = Time.now.utc
  save
end

Phương thức confirmation_token_valid? kiểm tra nếu xác nhận đã gửi trong 30 ngày và do đó chưa hết hạn. Bạn có thể thay đổi nó với giá trị bất kỳ theo ý mình.

mark_as_confirmed! lưu thời gian xác nhận và làm vô hiệu các token xác nhận để email xác nhận cùng có thể không được sử dụng để xác nhận lại người dùng.

Bây giờ chúng ta có endpoint để xác nhận một user. Bạn có thể kiểm tra nó bằng cách gửi một request post đến endpoint users/confirm?token=<CONFIRMATION_TOKEN> và kiểm tra giá trị confirmed_at và confirmation_token của user. Bạn nên lấy User đã xác nhận thành công. Các request theo sau với cùng một token phải trả lại token không hợp lệ.

Bây giờ chúng ta có thể thực hiện đăng ký như một phần của ứngdụng.

6. Login

6.1. Controller

Hãy thêm route login vào file config/routes.rb dưới resource user:

resources :users, only: :create do
    collection do
      post 'confirm'
      post 'login'
    end
end

Chúng ta đã tạo một route users/login. Trong controllers yêu cầu một vài thay đổi để lấy code trong json_web_token và tạo action tương ứng trong UsersController.

Trong controllers/application_controller.rb, thêm dòng dưới sau định nghĩa class:

require 'json_web_token'

Trong controllers/users_controller.rb thêm các dòng như sau:

def login
  user = User.find_by(email: params[:email].to_s.downcase)

  if user && user.authenticate(params[:password])
      auth_token = JsonWebToken.encode({user_id: user.id})
      render json: {auth_token: auth_token}, status: :ok
  else
    render json: {error: 'Invalid username / password'}, status: :unauthorized
  end
end

Tìm hiểu phương thức này từng bước. Đầu tiên Chúng ta sẽ lấy user từ email và nếu tồn tại, gọi phương thức xác thực qua password đã được cung cấp. Phương thức xác thực đượccung cấp bằng helper has_secure_password.

Một khi chúng tôi xác minh email và mật khẩu, mã hóa id của user vào một token JWT qua phương thức mã hóa của chúng tôi từ JsonWebToken lib mà chúng ta vẫn chưa tạo ra. Sau đó, trả lại token.

Đối với những người được xác nhận email, chúng tôi có để không cho phép những người dùng không được xác nhận. Sửa đổi các hành động điều khiển bao gồm các điều kiện một hơn:

...
if user && user.authenticate(params[:password])
  if user.confirmed_at?
    auth_token = JsonWebToken.encode({user_id: user.id})
    render json: {auth_token: auth_token}, status: :ok
  else
    render json: {error: 'Email not verified' }, status: :unauthorized
  end
else
...

kiểm tra nếu trường confirmed_at không rỗng là làm việc, có nghĩa là user đã được xác nhận trước khi cho phép họ đăng nhập.

6.2. JWT Library

Bây giờ hãy thêm thư viện JWT. Bắt đầu bằng cách thêm gem dưới đây vào Gemfile của bạn và chạy bundle instal:

gem 'jwt'

Sau khi thực hiện, tạo ra một file gọi là jsonwebtoken.rb dưới lib/ và thêm những dòng này:

require 'jwt'

class JsonWebToken
  # Encodes and signs JWT Payload with expiration
  def self.encode(payload)
    payload.reverse_merge!(meta)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

  # Decodes the JWT with the signed secret
  def self.decode(token)
    JWT.decode(token, Rails.application.secrets.secret_key_base)
  end

  # Validates the payload hash for expiration and meta claims
  def self.valid_payload(payload)
    if expired(payload) || payload['iss'] != meta[:iss] || payload['aud'] != meta[:aud]
      return false
    else
      return true
    end
  end

  # Default options to be encoded in the token
  def self.meta
    {
      exp: 7.days.from_now.to_i,
      iss: 'issuer_name',
      aud: 'client',
    }
  end

  # Validates if the token is expired by exp parameter
  def self.expired(payload)
    Time.at(payload['exp']) < Time.now
  end
end

Bây giờ chúng ta có chức năng login endpoint mà chúng ta có thể sử dụng để đăng nhập user. Thử gọi endpoint users/login với các dữ liệu dưới định dạng trong body yêu cầu:

{
  "email": "test@example.com",
  "password": "anewpassword"
}

Bạn sẽ thấy một response tương tự như sau:

{
  "auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NzUzMTM5OTQsImlzcyI6Imlzc3Vlcl9uYW1lIiwiYXVkIjoiY2xpZW50In0.5P3qJKelCdbTixnLyIrsLKSVnRLCv2lvHFpXqVKdPOs"
}

Nó là token xác nhận của chúng tôi cho user. Bây giờ chúng ta có thể sử dụng token này để xác nhận từng yêu cầu cho user.

6.3. Authentication Helper

Chúng ta sẽ tạo một phương thức helper để lấy token từ header, validates các payload, và lấy các user tương ứng từ DB. Mở file /app/controllers/application_controller.rb và thêm vào như sau:

protected
# Validates the token and user and sets the @current_user scope
def authenticate_request!
  if !payload || !JsonWebToken.valid_payload(payload.first)
    return invalid_authentication
  end

  load_current_user!
  invalid_authentication unless @current_user
end

# Returns 401 response. To handle malformed / invalid requests.
def invalid_authentication
  render json: {error: 'Invalid Request'}, status: :unauthorized
end

private
# Deconstructs the Authorization header and decodes the JWT token.
def payload
  auth_header = request.headers['Authorization']
  token = auth_header.split(' ').last
  JsonWebToken.decode(token)
rescue
  nil
end

# Sets the @current_user with the user_id from payload
def load_current_user!
  @current_user = User.find_by(id: payload[0]['user_id'])
end

Ở đây, authenticate_request! là phương thức helper mà chúng ta sẽ sử dụng để xác thực các hành động controller. Nó lấy payload từ header Authorization của request, sau đó validates payload sử dụng các phương thức valid_payload mà chúng ta đã xem ở trên. Sau khi xác nhận hợp lệ, rồi nó lấy user sử dụng user_id trong payload, tải bản g vào scope.

Chúng ta có thể thêm phương thức authenticate_request như một before_filter đến bất kỳ hành động controller mà chúng ta muốn xác thực cho user.

Kết luận:

Cảm ơn các bạn đã theo dõi bài viết, mong rằng nó sẽ giúp ích cho các bạn!

Tham khảo: https://www.sitepoint.com/authenticate-your-rails-api-with-jwt-from-scratch/

0