- vừa được xem lúc

Xây dựng api cho sample app với grape

0 0 19

Người đăng: Ngoc Vu

Theo Viblo Asia

Hướng dẫn này mô tả cách xây dựng 1 số api cơ bản cho sample_app sử dung Grape

I. Cài đặt

Thêm gem:

gem "grape"
gem "grape_on_rails_routes"
gem "grape-entity"

Nhớ chạy bundle install để thêm gem vào nhé 😄

II. Build api trả về danh sách users

Sau khi cài đặt thì giờ chúng ta xây dựng 1 api trả về danh sách users

B1: Tạo thư mục api và base

Định nghĩa 1 thư mục api trong folder controller

mkdir -p app/controllers/api

Tiếp đến chúng ta xây dựng 1 file base trong folder api

touch app/controllers/api/base.rb

Sau đó chúng ta thêm đoạn code này vào base.rb

module API class Base < Grape::API mount API::V1::Base # mount API::V2::Base (next version) end
end 

mount API::V1::Base : Đây là đường dẫn để tìm api, có bao nhiêu api ta sẽ viết hết trong Base của V1

Khai báo routes:

mount API::Base, at: "/"

Các bạn có thể để ý thấy rằng mọi thứ đều bắt đầu từ base.rb

Chú ý: Các bạn phải thêm đoạn code này vào file inflections.rb

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "API"
end

Nếu k server sẽ báo lỗi

/home/vu.thi.ngoc/Projects/sample_app/config/routes.rb:3:in `block in <main>': uninitialized constant API (NameError)
Did you mean? Api

B2: Thêm version cho api

Tiếp đến ta define một version api có tên là V1

# controllers/api/v1/base.rb
module API module V1 class Base < Grape::API mount V1::Users # mount API::V1::AnotherResource end end
end 

mount V1::Users : Khai báo đường dẫn

Để lấy danh sách users chúng ta sẽ định nghĩa 1 class Users trong v1

# controllers/api/v1/users.rb
module API module V1 class Users < Grape::API prefix "api" version "v1", using: :path format :json resource :users do desc "Return all users" get "", root: :users do users = User.all present users end end end end
end

Ở đây

  • version ‘v1’, using: :path: chỉ định version của API.
  • format: json: nói với các API rằng chúng ta chỉ sử dụng định dạng JSON.
  • prefix: api: hiểu đơn giản thì các path của chúng ta sẽ bắt đầu với /api. Nếu bạn chưa quên thì trong routes.rb chúng ta đã set route như này: mount API::Base, at: "/" Với prefix như trên chúng ta sẽ truy cập vào API theo đường dẫn /api.
  • resource :users: nói rằng chúng ta sẽ sử dụng user routes
  • desc ‘Return all users’: mô tả api sẽ trả về gì
  • get do …end: cái này thì khá cơ bản rồi, nó cũng giống như trong controller bình thường.

B3: Kiểm tra routes

Chạy rails grape:routes để nhìn toàn bộ các api có trong hệ thống, ở đây ta đã implement api trả về danh sách các users

 GET | /api/:version/users(.json) | v1 | Return all users

B4: Chạy kiểm tra kết quả

Dùng postman, chọn phương thức GET với đường dẫn localhost:3000/api/v1/users , chọn Send và nhìn kết quả trả về dưới dạng

[ { "id": 1, "name": "admin", "email": "_@.com", "created_at": "2020-09-28T04:31:45.741Z", "updated_at": "2020-09-29T03:08:56.596Z", "password_digest": "$2a$12$WlS2CtP5TIvJEVE4Gn9sQuexqTxRB8KSMoUq7DhVH.ufeet3foyP2", "remember_digest": "$2a$12$T9W6yDKhgDcU5evfp0M2HOzecCEyyJP4942bLse77ppS63ZLA1MTm", "admin": true, "activation_digest": "$2a$12$tBO3ucVUe1HHJduKIWf/pe4F8E8ayWseNiPsRqyY17WNkndTbLNfS", "activated": true, "activated_at": "2020-09-28T04:31:45.000Z", "reset_digest": null, "reset_sent_at": null }, { "id": 2, "name": "Fr. Efrain Lebsack", "email": "_@.com", "created_at": "2020-09-28T04:31:46.932Z", "updated_at": "2020-09-29T03:02:46.427Z", "password_digest": "$2a$12$zslwRliHClDLBeAR4bunUe2rSOOm0IqQ624/9ZcTjFiXX5ZGgiV46", "remember_digest": "$2a$12$0wdlrqbQKQRNvds4qMB3qutOo7i9q31unL2OaARGoJei5P3LYV5Te", "admin": false, "activation_digest": "$2a$12$wVtB1ngTBir2yFl4C63zGuSEAE9hmRXkHn/anuDKmF/Xs8tmWiuc2", "activated": false, "activated_at": "2020-09-28T04:31:46.000Z", "reset_digest": null, "reset_sent_at": null },... ]

là thành công rồi đó, ta đã lấy được danh sách các user có trong hệ thống

III. Xử lý response

Trong bài toán thực tế ít khi api trả về toàn bộ thông tin user như bên trên, 1 số trường sẽ k cần thiết phải trả về hay muốn custom trường trả về thì sẽ ntn. Giờ là lúc ta xử lý các entity với grape-entity. grape-entity cho phép chúng ta định nghĩa các thuộc tính nào trả về.

Giả sử cần trả về danh sách user chỉ bao gồm tên và email

# app/controllers/api/entities/user.rb
module API module Entities class User < Grape::Entity expose :name expose :email end end
end

Sửa 1 chút phần api users, thêm with: API::Entities::User để lọc các giá trị trả về

resource :users do desc "Return all users" get "", root: :users do users = User.all present users, with: API::Entities::User end
end

Nhìn kết quả trả về

[ { "name": "admin", "email": "_@.com" }, { "name": "Fr. Efrain Lebsack", "email": "_@.com" }, .. ]

vậy là đã xong rồi đó 😄

IV. Build api trả về user detail

# app/controllers/api/v1/users.rb
module API module V1 class Users < Grape::API ... desc "Return a user" params do requires :id, type: String, desc: "ID of the user" end get ":id", root: "user" do user = User.find params[:id] present user, with: API::Entities::User end end end end
end

Gọi api localhost:3000/api/v1/users/1 sẽ trả về thông tin của user có Id là 1

{ "name": "admin", "email": "_@.com"
}

Nếu gọi 1 ID quá lớn không tồn tại thì sao? localhost:3000/api/v1/users/1111 Sẽ bị lỗi ngay lập tức

ActiveRecord::RecordNotFound (Couldn't find User with 'id'=1111):

V. Xử lý lỗi và ngoại lệ

Chúng ta sẽ tạo 1 module là default chứa tất cả các code dùng chung cho tất cả api hoặc 1 số

module API module V1 module Defaults extend ActiveSupport::Concern included do prefix "api" version "v1", using: :path format :json rescue_from ActiveRecord::RecordNotFound do |e| error_response(message: e.message, status: 404) end rescue_from :all do |e| if Rails.env.development? raise e else error_response(message: e.message, status: 500) end end end end end
end

Dùng rescue để bắt 1 số ngoại lệ

Thay đổi users.rb: Thêm include API::V1::Defaults, nếu nó dc dùng cho tất cả các api trong v1 ta có thể include vào base.rb

module API module V1 class Users < Grape::API include API::V1::Defaults resource :users do desc "Return all users" get "", root: :users do users = User.all present users, with: API::Entities::User end desc "Return a user" params do requires :id, type: String, desc: "ID of the user" end get ":id", root: "user" do user = User.find params[:id] present user, with: API::Entities::User end end end end
end

Giờ chúng ta thử gọi api user detail vs 1 ID không tồn tại localhost:3000/api/v1/users/1111 Kết quả trả về, mặc định lỗi sẽ trả về ntn

{ "error": "Couldn't find User with 'id'=1111"
}

Chúng ta có thể kiểm soát các lỗi và custom chúng để trả về lỗi theo ý của mình

# app/controllers/api/error_formatter.rb
module API module ErrorFormatter def self.call message, backtrace, options, env, original_exception {response_type: "loi roi do", response: message}.to_json end end
end

Ta thêm nó vào base tổng để dùng cho toàn bộ api

# app/controllers/api/base.rb
module API class Base < Grape::API error_formatter :json, API::ErrorFormatter mount API::V1::Base end
end

Nếu chỉ override lỗi cho chỉ riêng V1 thì add vào base.rb của v1 error_formatter :json, API::ErrorFormatter

Giờ chúng ta nhìn message lỗi sau khi custom:

{ "response_type": "loi roi do", "response": "Couldn't find User with 'id'=1111122"
}

VI. Xác thực người dùng sử dụng JWT (Json web token)

JWT là gì ?

JWT là một phương tiện đại diện cho các yêu cầu chuyển giao giữa hai bên Client – Server , các thông tin trong chuỗi JWT được định dạng bằng JSON . Trong đó chuỗi Token phải có 3 phần là header , phần payload và phần signature được ngăn bằng dấu “.” Vậy theo lý thuyết trên thì mình sẽ có một chuỗi Token như sau :

 header.payload.signature

Đọc thêm tại : https://viblo.asia/p/json-web-token-jwt-la-gi-3Q75wWOG5Wb

Tại sao lại cần sử dụng jwt

Ví dụ ta yêu cầu Server lấy User có Id là 01 như sau [GET] localhost:8080/users/01 hoặc xóa [DELETE] localhost:8080/users/01. Ở đây nếu chúng ta không sử dụng bất kì phương thức nào để bảo mật API thì tất cả các User khác điều có thể gọi tới các Request này để lấy thông tin hoặc xoá User 01 và Server sẽ thực hiện yêu cầu mà không cần biết yêu cầu này có phải là của User 01 hay không . Điều này rất nguy hiểm , các hacker có thể xóa hết dữ liệu hoặc đánh cắp thông tin người dùng bằng cách truy cập vào các URL này , cho nên ta cần một phương pháp nào đó để Server xác định được yêu cầu đó là của User01 thì Server mới thực hiện , vì vậy ta sẽ sử dụng JWT

Cách thức hoạt động JWT

Bước 1 : Người dùng yêu cầu đăng nhập với Username , Password

Bước 2 : Server nhận được yêu cầu và kiểm tra Username , Password nếu đúng sẽ gửi cho người dùng một chuỗi JWT

Bước 3 Người dùng sẽ dùng JWT này kèm theo các yêu cầu kế tiếp

Bước 4: Server sẽ nhận yêu cầu và kiểm tra chuổi JWT , nếu chuỗi hợp lệ thì sẽ thực hiện yêu cầu

Bạn có thể đọc thêm về jwt tại đây https://viblo.asia/p/tai-sao-phai-su-dung-json-web-token-jwt-de-bao-mat-api-maGK787AZj2

Xây dựng thư viện JWT Authentication

  • Đầu tiên chúng ta thêm gem jwt vào Gemfile
gem "jwt"

Vì chúng ta sử dụng encode và decode JWT khá nhiều, nên sẽ thuận tiện hơn nếu viết một class bao gồm các chức năng đó.

  • Tạo 1 file với đường dẫn lib/authentication.rb
  • Sau đó thêm đường dẫn autoload trong config/application.rb
config.autoload_paths << Rails.root.join('lib')

Trong class authentication ta định nghĩa 2 method chịu trách nhiệm tạo jwt từ thông tin người dùng và decode để giải mã jwt

# lib/authentication.rb
require "jwt" class Authentication ALGORITHM = "HS256" class << self def encode payload JWT.encode(payload, ENV["AUTH_SECRET"], ALGORITHM) end def decode token JWT.decode(token, ENV["AUTH_SECRET"], true, {algorithm: ALGORITHM}).first end end
end

Encode phục vụ để mã hóa trả về jwt

JWT.encode: method này có 3 đối số

  1. Dữ liệu ở dạng băm, chúng ta sẽ mã hóa trong JWT
  2. Chìa khóa cho thuật toán băm của bạn
  3. Các loại thuật toán băm Ví dụ:
payload = {name: "sophie"}
secret_key = "masd82348$asldfja"
algorithm = "HS256" JWT.encode(payload, secret_key, algorithm)
=> "esdiva23euihrusdfcnkjz2snciusdhuihr7480y2qikjh8"

Chúng ta sẽ tạo secret_key bằng cách nào ? Sử dụng module Digest có sẵn từ Ruby để tạo khóa bí mật. Chúng ta sẽ tạo khóa trên rails console, thêm nó vào môi trường của chúng ta dưới dạng một biến môi trường ENV["AUTH_SECRET"]. Chúng ta sẽ sử dụng Figaro để đảm bảo rằng biến môi trường không bị đẩy lên GitHub.

irb(main):002:0> Digest::SHA256.digest('ngocvu')
=> "\x83\xAF6$\xDA^\x9B\nL%\x1F,q\xF4;}\xF1\x1F\x9B)3\x17}m\x98\x8C\x80\xDA\xB7\xAE\xF2\\"

Trong file application.yml (file này lưu biến môi trường k dc đẩy lên github đâu nha)

AUTH_SECRET: "\x83\xAF6$\xDA^\x9B\nL%\x1F,q\xF4;}\xF1\x1F\x9B)3\x17}m\x98\x8C\x80\xDA\xB7\xAE\xF2\\"

Decode phục vụ để giải mã

Phương thức này có ba đối số:

  1. JWT muốn giải mã,
  2. Khóa bí mật của thuật toán băm
  3. Các loại thuật toán băm

Thường chúng ta encode Authentication.encode({user_id: user.id}) đầu vào là user_id và giải mã decode để lấy user_id

Login trả về JWT

Ta định nghĩa

module API module V1 class Auth < Grape::API include API::V1::Defaults helpers do def represent_user_with_token user present jwt_token: Authentication.encode({user_id: user.id}) end end resources :auth do desc "Sign in" params do requires :email requires :password end post "/sign_in" do user = User.find_by email: params[:email] if user&.authenticate params[:password] represent_user_with_token user else error!("Invalid email/password combination", 401) end end end end end
end

trong base v1: thêm mount V1::Auth

module API module V1 class Base < Grape::API mount V1::Users mount V1::Auth # mount API::V1::AnotherResource end end
end

User đăng nhập mới lấy được danh sách user

Ở usesr.rb trước khi vào các action ta xác thực user trước bằng hàm authenticate_user!

# app/controllers/api/v1/users.rb
module API module V1 class Users < Grape::API include API::V1::Defaults before do authenticate_user! end resource :users do desc "Return all users" get "", root: :users do users = User.all present users, with: API::Entities::User end desc "Return a user" params do requires :id, type: String, desc: "ID of the user" end get ":id", root: "user" do user = User.find params[:id] present user, with: API::Entities::User end end end end
end

function authenticate_user! sẽ được dùng ở nhiều nơi nên ta định nghĩa trong defaults.rb

module API module V1 module Defaults extend ActiveSupport::Concern #.... included do helpers do def authenticate_user! token = request.headers["Jwt-Token"] user_id = Authentication.decode(token)["user_id"] if token @current_user = User.find_by_id user_id unless @current_user api_error!("You need to log in to use the app", "failure", 401, {}) end end end end end end
end

Hàm authenticate_user! sẽ decode jwt-token để lấy dc user_id sau đó sẽ tìm kiếm trong database. nếu user đó không tồn tại thì sẽ báo lỗi

Giờ ta chạy lại ta cần gắn jwt-token vào header thì ms lấy được danh sách user nha ✌️

Thử không truyền lên jwt-token xem hệ thống báo gì nhé

Tất nhiên không chết rồi, chúng ta đã bắt lỗi api_error!("You need to log in to use the app", "failure", 401, {})

References

https://github.com/awesome-academy/sample-app-api

Bình luận

Bài viết tương tự

- vừa được xem lúc

Docker: Chưa biết gì đến biết dùng (Phần 3: Docker-compose)

1. Mở đầu. . .

0 0 106

- vừa được xem lúc

Tích hợp VNPAY vào Rails

Xin chào 500 ae năm mới nhé. Tiếp nối câu chuyện về Thanh toán online mà mình có chia sẽ ở 2 bài trước, mọi người chưa đọc thì có thể vào xem ở đây nhé.

1 1 84

- vừa được xem lúc

Tìm hiểu Adapter Pattern trong Rails

. Nếu là một web developer chắc hẳn chúng ta đã không ít lần đọc qua về các Design patterns hay cách áp dụng chúng để làm cho code trở nên hướng đối tượng hơn, dễ đọc, dễ hiểu, dễ maintain, dễ mở rộng, … Các design patterns được áp dụng khá nhiều trong các Rails projects như Service Object, Decorato

0 0 35

- vừa được xem lúc

Sử dụng Searchkick để tìm kiếm thông minh trên Rails và Elasticsearch

Bạn đã bao giờ tự hỏi, ứng dụng web của mình có thể mở rộng quy mô bằng cách học được các từ khóa mà người dùng tìm kiếm? Có giải pháp nào cung cấp công cụ tìm kiếm tự động nhanh chóng với chỉ 1 từ khóa bất kì? Thật may khi có Searchkick và Elasticsearch là các công cụ hỗ trợ công việc tìm kiếm trở

0 0 90

- vừa được xem lúc

Những sai lầm bạn có thể mắc phải khi code Rails

. Chào các bạn, chào các bạn. Đừng vội đóng tab nha.

0 0 35

- vừa được xem lúc

Một số lưu ý cải thiện performance khi làm việc với Rails

Khi làm việc với ruby on rails chắc hẳn chúng ta sẽ làm việc với active record rất nhiều. Tuy nhiên có nhiều điều có thể ta vẫn chưa thực sự hiểu, ví dụ như ActiveRecord execute SQL query như thế nào? Và cũng còn khá nhiều lập trình viên khác cũng không để ý tới điều này.

0 0 98