Series Hướng Dẫn Lập Trình Ruby on Rails (Phần 9) Hướng dẫn xây dựng ứng dụng API đơn giản với gem doorkeeper

Chào các bạn,

Giới thiệu

Hôm nay mình sẽ tiếp tục Series Hướng Dẫn Lập Trình Ruby on Rails, trong bài này mình sẽ hướng dẫn các bạn cách xây dựng một ứng dụng API đơn giản. Mình sẽ tiếp tục làm trên project đã có sẵn từ trước đến nay đó là cái Login App của chúng ta. Bạn này quên hoặc chưa có thì có thể download nó lại tại đây 😄

Công cụ

Trong bài này để xây dựng một ứng dụng API mình sẽ sử dụng

  • Gem doorkeeper : để generate access token.
  • Gem active_model_serializers: để hỗ trợ customize các attributes mà mình muốn trả về trong API
  • Postman: công cụ để test API. Nếu các bạn chưa biết dùng POSTMAN thì các bạn có thể tham khảo một bài viết khá hay ở đây

OK. Như vậy phần chuẩn bị đã xong. Chúng ta bắt tay vào thực hiện nhé.

Tiến hành

Cài đặt gem Doorkeeper

Đầu tiên chúng ta cần add 2 cái gems trên vào Gemfile và chạy bundle

gem "doorkeeper"

sau đó chạy bundle install

Để cài đặt gem "doorkeeper" tiếp theo chạy lệnh:

rails g doorkeeper:install

Sau khi chạy lệnh trên nó sẽ sinh ra cho chúng ta file /config/initializers/doorkeeper.rb dùng để config các tính năng của gem như:

  • Thời gian expires của access token: access_token_expires_in. Mặc định nếu không thay đổi sẽ là 2 hours
  • Sử dụng refresh access token: use_refresh_token
  • Config resource owner
  • ...

Ngoài ra còn thêm nhiều phần khác các bạn có thể vào trang github của nó để đọc nhé :v Trong phần này mình sẽ dùng chức năng refresh_token nên sẽ enable nó lên

Để generate file migration mặc định của doorkeeper chạy:

rails g doorkeeper:migration

sau khi generate thì nó sinh ra cho chúng ta cái file migration như này:

Tuy nhiên chúng ra cần chỉnh sửa lại một chút :v Vì trong bài viết này chúng ta cần sinh ra token cho những user trong model User đã được đăng ký từ Web LoginApp từ trước. Do đó ta sẽ có cấu trúc quan hệ ở đây là one user has_many oauth_access_tokens

Mở file migration và chỉnh sửa lại phần add_foreign_key như bên dưới:

.
.
.
    add_index :oauth_access_tokens, :token, unique: true
    add_index :oauth_access_tokens, :resource_owner_id
    add_index :oauth_access_tokens, :refresh_token, unique: true
    add_foreign_key(
      :oauth_access_tokens,
      :users,
      column: :resource_owner_id
    )

sau đó chạy

rake db:migrate

Nó sẽ sinh ra cho chúng ta 3 cái table:

  • oauth_access_tokens
  • oauth_access_grants
  • oauth_applications

Đối với lần hướng dẫn này, chúng ta chỉ cần lưu ý đến table oauth_access_tokens vì nó sẽ chứa thông tin cần dùng để authenticate như: token, expires_in, revoked_at,...

Thêm cái relationship has_many access_token vào model user

class User < ApplicationRecord
  has_secure_password

  has_many :access_tokens, class_name: "Doorkeeper::AccessToken",
    foreign_key: :resource_owner_id, dependent: :destroy
end    

ok như vậy cơ bản là xong phần cài đặt.

Viết API

Trong bài này chúng ta sẽ xây dựng 3 cái API:

  • User login để lấy access token
  • User logout: revoke access token
  • Get list users hiện tại

application_controller.rb

Để project chúng ta được clear: chúng ta sẽ đặt các api vào trong namespace là api

  • Tạo thư mục api bên trong thư mục controllers
  • Tạo một file application_controller.rb dùng cho các api khác kế thừa, bản thân nó sẽ kế thừa từ lớp ActionController::API mà rails đã cung cấp sẵn cho chúng ta.

application_controller.rb

class Api::ApplicationController < ActionController::API
  before_action :doorkeeper_authorize!
  before_action :current_user

  def doorkeeper_unauthorized_render_options error
    {json: {errors: I18n.t("doorkeeper.errors.messages.unauthorized_client", uid: current_user&.id)}}
  end

  def current_user
    @current_user ||= User.find_by(id: doorkeeper_token.resource_owner_id) if doorkeeper_token
  end
end

:doorkeeper_authorize! là một method trong helper mà Doorkeeper cung cấp sẵn cho chúng ta dùng để authenticate người dùng khi họ gửi token lên

sessions_controller.rb

Controller này sẽ chứa 2 api: login và logout

ta sẽ chọn action #create cho việc thực hiện login và action #destroy cho việc thực hiện logout của user

Trong API login: user cần phải gửi name và password lên để chúng ta trả về access token cho user ấy. Trong API logout: user chỉ cần gửi access token mà đã nhận được lên để chúng ta thực hiện hủy giá trị của token ấy đi là xong.

Việc lưu trữ access token sẽ được phía Client thực hiện.

Ta sẽ có file sessions_controller.rb như thế này

class Api::SessionsController < Api::ApplicationController
  skip_before_action :doorkeeper_authorize!, only: :create

  def create
    user = User.authenticate users_params
    if user
      render json: {data: Api::GenerateTokenService.new(user).call}, status: 200
    else
      render json: {errors: I18n.t("errors.api.sessions_controller.login.failed")}, status: 400
    end
  end

  def destroy
    revoke_token_service = Api::RevokeTokenService.new doorkeeper_token.token
    revoke_token_service.execute
    if revoke_token_service.success?
      render json: {}, status: 200
    else
      render json: {errors: revoke_token_service.errors}, status: 400
    end
  end

  private
  def users_params
    params.require(:user).permit :name, :password
  end
end

Đầu tiên là thằng

user = User.authenticate users_params

dùng dể chứng thực name và password mà user gửi lên có đúng hay không. đã có thông tin input và output vào model user viết nó nào:

user.rb

class User < ApplicationRecord
.
.
.
  class << self
    def authenticate params
      user = User.find_by name: params[:name]
      return false unless user
      user.authenticate params[:password]
    end
  end
end

Ta sẽ có hàm authenticate như này, đơn giản đúng không.

Tiếp theo:

Api::GenerateTokenService.new(user).call

Service này dùng để gen cho chúng ta 1 cái access token sau khi user đã được chứng thực thành công.

trong thư mục app các bạn tạo thêm thư mục services/api, sau đó chúng ta sẽ tạo 1 class Api::GenerateTokenService dùng để gen access token như bên dưới

class Api::GenerateTokenService
  attr_reader :user

  def initialize user
    @user = user
  end

  def call
    generate_token user
  end

  private
  def generate_token user
    access_token = Doorkeeper::AccessToken.create! resource_owner_id: user.id,
      expires_in: Doorkeeper.configuration.access_token_expires_in, use_refresh_token: true
    token_info = Doorkeeper::OAuth::TokenResponse.new(access_token).body
        .merge uid: user.id, name: user.name
    created_at = token_info["created_at"]
    token_info["created_at"] = Time.zone.at(created_at).iso8601
    token_info.merge expires_on: Time.zone.at(created_at + token_info["expires_in"]).iso8601
  end
end

Như vậy là đã xong.

Tiếp theo:

Api::RevokeTokenService.new doorkeeper_token.token

Service RevokeTokenService dùng để hủy một cái access token khi user thực hiện logout, hoặc sau này các bạn cho thể tận dụng để thực hiện hủy cái access token ở một chỗ nào khác nếu muốn :v.

Ta sẽ có như sau:

class Api::RevokeTokenService
  attr_reader :token, :errors

  def initialize token
    @token = token
  end

  def execute
    revoke_token token
  end

  def success?
    errors.nil?
  end

  private
  def revoke_token token
    access_token = Doorkeeper::AccessToken.find_by token: token
    if access_token
      access_token.revoke
    else
      @errors = I18n.t "doorkeeper.errors.messages.invalid_token.unknown"
    end
  end
end

Phần viết Controller và Service như vậy là đã ổn, giờ chỉ việc vào routes cấu hình và test thử nữa là ok.

Thêm vào file routes.rb như bên dưới:

Rails.application.routes.draw do
  use_doorkeeper do
    skip_controllers :applications, :authorized_applications
  end

  namespace :api do
    post "login"  => "sessions#create"
    delete "logout" => "sessions#destroy"
  end

  get     "login"    => "sessions#new"
  post    "login"    => "sessions#create"
  delete  "logout"   => "sessions#destroy"

  resources :users
end

Như vậy là ok rồi

Mở rails s và test thử thành quả nào :v

API: api/login

api/login

API: api/logout

api/logout

users_controller.rb

Bây giở chúng ta sẽ viết API để get list users hiện tại nhé. Để get list users chúng ta sẽ sử dụng action #index của controller này.

Chúng ta sẽ có như sau:

class Api::UsersController < Api::ApplicationController
  before_action :users, only: :index

  def index
    render json: @users, status: 200
  end

  private
  def users
    @users = User.all
  end
end

Thêm vào routes.rb

namespace :api do
  .
  .
  .
  resources :users
end

OK chúng ta sẽ gọi thử API get list users nhé. Để gọi API này chúng ta cần phải truyển lên access token mà hồi nãy sau khi thực hiện gọi api login nhận được: ví dụ: mình có access token là 8f2d221b119878907e14d7face59ef5a108813069c410b3305a67a5fb7ab1c3c

Thì header khi gọi API list users sẽ như sau

Authorization: Bearer 8f2d221b119878907e14d7face59ef5a108813069c410b3305a67a5fb7ab1c3c Content-Type: json

Ngon chạy được rồi đấy =]]

Tuy nhiên ở đây chúng ta thấy có field password_digest là field mà chúng ta không mong muốn trả về vì đây là chuỗi mã hóa mật khẩu của user.

Vậy chúng ta cần loại nó ra khỏi response trả về. Vì mặc định rails nó sẽ generate tất cả attributes đang có trong model thành json rồi trả về. Do đó để customize được các attributes chúng ta sẽ sử dụng gem active_model_serializers

Cài đặt gem active_model_serializers

Đưa cái này vào Gemfile rồi chạy bundle install

gem "active_model_serializers", "~> 0.10.0"

Để customize được các attributes cần trả về của model User chúng ta cần tạo 1 class serializer của model User.

Bằng cách:

rails g serializer user

gem active_model_serializers sẽ tự động generate cho chúng ta class UserSerializer

class UserSerializer < ActiveModel::Serializer
  attributes :id
end

Chúng ta sẽ có như trên. Giờ muốn response về attributes nào của model user chỉ cần điền vào phần attributes ở trên là được.

Mình sẽ customize nó lại như sau:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :created_at
end

Mở rails s và chạy lại xem nào

OK bây giờ nó chỉ response về những attributes mà mình muốn. Ngoài ra thằng active_model_serializers còn cho phép chúng ta thực hiện generate theo relationship và nhiều cái khác nữa. Các bạn chịu khó tìm hiểu thêm trên trang Github của nó nhé 😄.

Link full source code: https://github.com/duc11t3bk/login_app

OK. Hôm nay mình tạm dừng tại đây. Hẹn các bạn vào kỳ tới nhé :v


All Rights Reserved