Xây dựng một API hoàn chỉnh với Rails 5
TIL
499
White

Bùi Quang Thăng viết ngày 22/07/2017

Ở phiên bản rails 5, thì gem rails-api đã được tích hợp vào phần core của Rails, vì vậy chúng ta có thể khởi tạo API trong Rails 1 cách dễ dàng và nhanh chóng. Cho đến bây giờ, Grape vẫn được xem là sự lựa chọn tốt nhất để viết API trong Ruby, vậy nếu mình đã quen với cách việc với Rails thuần thì sao ? Trong bài viết này mình sẽ hướng dẫn cơ bản cho các bạn cách để tạo một API đơn giản, step by step trong Rails 5. Bài viết của mình có những mục sau:

  1. Cài đặt project với chế độ API trong rails 5
  2. Sử dụng Rspec để testing
  3. Xây dựng API
  4. Serializing API Output
  5. Enabling CORS
  6. Phiên bản API
  7. Bảo vệ API cuả bạn
  8. Authentication API
  9. Viết document cho API sử dụng Swagger UI

1. Cài đặt project với chế độ API trong rails 5

Đầu tiên, hãy chắc chắn rằng bạn đã cài phiên bản Ruby 2.2.2 trở lên và Rails version 5.
Theo hướng dẫn của Rails guide, thì tất cả những gì chúng ta cần làm để tạo một API trong rails là thêm --api phía sau câu lệnh khởi tạo project mới trong Rails.
rails new api_app_name --api
Tiếp theo chúng ta chạy lệnh bundle để thực hiện cài đặt các gem mặc định và tiến hành cài đặt database trong thư mục chứa project.

> cd  <parent-folder-path>/api_app_name
> bundle install
> rails db:setup

2. Sử dụng Rspec để testing

Sau khi cài đặt xong project, chúng ta tiến hành cài đạt RSpec để tiến hành việc testing cho API. Lý do tại sao chúng ta tiến hành cài đặt RSpec đầu tiên vì nó sẽ giúp chúng ta tiết kiệm thời gian bằng cách sử dụng RSpec, thì nó sẽ tiến hành tạo tự động các file test controller và model khi chúng ta sử dụng câu lệnh rails generate scaffold để tự tạo các resources nhanh chóng. Để cài đặt RSpec, chúng ta thêm gem rspec-rails vào Gemfile trong nhóm :development, :test:

group :development, :test do
    gem '"rspec-rails"
    gem "factory_girl_rails"
end

Tiến hành cập nhật lại bundle, sau đó cài đặt RSpec vào project của chúng ta.

> bundle
> bin/rails g rspec:install

3. Xây dựng API

Sau khi cài đặt xong thì chúng ta sẽ tiến hành xây dựng API, sử dụng câu lệnh scaffold để khởi tạo model và controller tương ứng.

> bin/rails g scaffold user name email

Nó sẽ sinh ra các file có cấu trúc như sau:

          invoke  active_record
       identical    db/migrate/20151222022044_create_users.rb
       identical    app/models/user.rb
          invoke    rspec
          create      spec/models/user_spec.rb
          invoke      factory_girl
          create        spec/factories/users.rb
          invoke  resource_route
           route    resources :users
          invoke  scaffold_controller
       identical    app/controllers/users_controller.rb
          invoke    rspec
          create      spec/controllers/users_controller_spec.rb
          create      spec/routing/users_routing_spec.rb
          invoke      rspec
          create        spec/requests/users_spec.rb

Chú ý rằng do đây là API nên sẽ có thư mục view được tạo ra. Bây giờ chúng ta sẽ tiến hành migrate database và chạy app nhé.

> bin/rails db:migrate
> bin/rails s

API của chúng ta bây giờ đã chạy ở địa chỉ https://localhost:3000. Tuy nhiên đây chưa phải là điều quan trọng nhất, vì tất cả chỉ dừng ở mức cơ bản, và chúng ta còn nhiều việc phải làm để tạo thành một API hoàn chỉnh.

4. Serializing API Output

Trong API, thì dữ liệu của chúng ta trả về sẽ dưới định dạng JSON, tuy nhiên làm cách nào để chuyển dữ liệu từ database sang json một cách dễ dàng và đơn giản nhât. May mắn thay, Active Model Serializers đã giúp ta giải quyết vấn đề đó. Tiến hành cài đặt gem active_model_serializers bằng cách thêm vào Gemfile:

gem 'active_model_serializers'

Cập nhật bundle và tiến hành khởi tạo serializer tương ứng với model của chúng ta, ở đây là model User.

> bundle
> rails g serializer user

Trong file app/serializers/user_serializer.rb chúng ta sẽ có đoạn code như sau:

class UserSerializer < ActiveModel::Serializer
    attributes :id
end

Mặc định sẽ khởi tạo một thuộc tính là :id, tuy nhiên chúng ta cần nhiều thuộc tính hơn nữa để sử dụng, vì vậy chúng ta tiến hành thêm hai thuộc tính là :name:email vào serializer để có thể lấy toàn bộ thông tin cần thiết trong database để chuyển về kiểu JSON. Như vậy, có thể thấy, mỗi serializer sẽ ánh xạ với một model trong database, để có thể chuyển dữ liệu từ database sang kiểu JSON để trả về cho phía front end sử dụng.

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

Các bạn có thể vào trang chủ của nó để tìm hiểu thêm nhé gem active_model_serializers

5. Enabling CORS

Có lẽ nhiều bạn sẽ thắc mắc CORS là gì vậy. CORS là viết tắt của Cross-origin resource sharing, là một cơ chế đặc biệt cho phép resource đặt tại một domain này có thể được request từ một domain khác với domain đó, nghĩa là làm sao để có thể gửi và nhận dữ liệu giữa 2 server với nhau. Trong trường hợp bạn muốn public API của mình, thì enable CORS là việc cần làm nếu như bạn không muốn sử dụng JSONP, và hầu hết các trình duyệt hiện nay đều đã hỗ trợ CORS. Tìm hiểu thêm về CORS cũng như cơ chế hoạt động của nó, bạn có thể vào link này.
Để làm điều đó trong API của chúng ta, thì chỉ việc cài đặt gem rack-cors vào Gemfile:

gem 'rack-cors'

Chạy cập nhật bundle và thêm vào vài dòng code như ở dưới vào file config/application.rb. Ở ví dụ này, nó sẽ cho phép thực hiện các request như GET, POST hoặc bất cứ OPTIONS request nào khác từ bất kì đâu.

module YourApp
    class Application < Rails::Application
    # ...
        config.middleware.insert_before 0, "Rack::Cors" do
            allow do
                origins '*'
                resource '*', headers: :any, :methods => [:get, :post, :options]
            end
        end
    end
end

6. Phiên bản API

Trước khi release public API của bạn, bạn nên chia API của mình thành các phiên bản, để có thê nâng cấp và sửa chữa khi có phản hồi từ khách hàng, giúp bạn dễ dàng bảo trì và nâng cấp API của mình mà không làm ảnh hưởng đến quá trình sử dụng của người dùng.
Hướng dẫn này sẽ giúp bạn cài đặt version API của mình theo URL format sau:

> GET http://api.mysite.com/v1/users/

Có môt cách khác nữa là sử dụng subdomain, cả 2 cách đều được, bạn thích cái nào thì dùng cái đó thôi.
Ở trong thư mục app/controller, chúng ta sẽ có cấu trúc thư mục như sau:

app/controllers/
.
|-- api
|   |-- v1
|       |-- api_controller.rb
|       |-- users_controller.rb
|-- application_controller.rb

Và đây là những gì bên trong controller:

# app/controllers/api/v1/api_controller.rb

module Api::V1
    class ApiController < ApplicationController
    # Generic API stuff here
    end
end

# app/controllers/api/v1/users_controller.rb

module Api::V1
    class UsersController < ApiController
    # GET /v1/users
        def index
            render json: User.all
        end
    end
end

Tiếp theo cài đặt ở routes, trong file config/routes, ta viết như sau:

constraints subdomain: 'api' do
    scope module: 'api' do
        namespace :v1 do
            resources :users
        end
    end
end

7. Bảo vệ API cuả bạn

Để bảo vệ API của chúng ta khỏi các cuộc tấn công DDoS, brute force attacks, smurf attack hoặc là muốn giới hạn lượt truy cập của người dùng, chúng ta có thể sử dụng một rack middleware là Rack::Attack. Gem rack-attack cung cấp cho chúng ta những phương thức bảo vệ như sau:

  • whitelist: cho phép truy cập bình thường nếu thỏa mãn điều kiện mà chúng ta đặt ra.
  • blacklist: gửi thông báo từ chối truy cập cho một request nào đó.
  • throttle: phân quyền truy cập của người dùng.
  • track: theo dõi các request, cung cấp thông tin về các request.

Tiến hành thêm gem 'rack-attack vào Gemfile:

gem "rack-attack"

Sau khi chạy bundle, ta tiến hành include middleware vào file config/application.rb

module YourApp
    class Application < Rails::Application
    # ...
        config.middleware.use Rack::Attack
    end
end

Trong thư mục** config/initializers*, ta khởi tạo file có tên là **rack_attack.rb, giúp chúng ta có thể cài đặt các luật trong middleware **Rack::Attack*. Các bạn có thể tham khảo ví dụ cơ bản dưới đây:

class Rack::Attack

    # `Rack::Attack` is configured to use the `Rails.cache` value by default,
    # but you can override that by setting the `Rack::Attack.cache.store` value
    Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new

    # Allow all local traffic
    whitelist('allow-localhost') do |req|
    '127.0.0.1' == req.ip || '::1' == req.ip
    end

    # Allow an IP address to make 5 requests every 5 seconds
    throttle('req/ip', limit: 5, period: 5) do |req|
    req.ip
    end

    # Send the following response to throttled clients
    self.throttled_response = ->(env) {
    retry_after = (env['rack.attack.match_data'] || {})[:period]
    [
        429,
        {'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s},
        [{error: "Throttle limit reached. Retry later."}.to_json]
    ]
    }
end

Để xem tât cả các options để config, bạn vào trang chủ của gem rack-attack để tham khảo thêm.

8. Authentication API

API không lưu giữ cookies hay session nào để có thể xác thực người dùng. Bạn có thể tham khảo về OAuth để có thể thực hiện việc xác định session của người dùng. Tuy nhiên trong hướng dẫn này, mình sẽ không đề cập đến nó, ở đây mình sẽ làm rõ cách hoạt động của nó như thế nào mà k dùng công cụ của bên thứ 3.
Cách tốt nhất để thực hiện xác thực một API request là sử dụng HTTP token, mỗi người dùng sẽ có một API key, và nó sẽ được đính kèm trong HTTP Authorization header với mỗi request như sau:

Authorization: Token token="WCZZYjnOQFUYfJIN2ShH1iD24UHo58A6TI"

Đầu tiên chúng ta tạo file migration để thêm thuộc tính api_key và model User

rails g migration AddApiKeyToUsers api_key:string

Thêm hàm tạo api_key cho mỗi user mới được khởi tạo.

class User < ActiveRecord::Base
    # Assign an API key on create
    before_create do |user|
        user.api_key = user.generate_api_key
    end

    # Generate a unique API key
    def generate_api_key
        loop do
            token = SecureRandom.base64.tr('+/=', 'Qrt')
            break token unless User.exists?(api_key: token)
        end
    end
end

Ở controller , chúng ta có thể thực hiện việc xác thực người dùng bằng method authenticate_or_request_with_http_token:

class ApplicationController < ActionController::Base
    include ActionController::HttpAuthentication::Token::ControllerMethods

    # Add a before_action to authenticate all requests.
    # Move this to subclassed controllers if you only
    # want to authenticate certain methods.
    before_action :authenticate

    protected

    # Authenticate the user with token based authentication
    def authenticate
        authenticate_token || render_unauthorized
    end

    def authenticate_token
        authenticate_with_http_token do |token, options|
            @current_user = User.find_by(api_key: token)
        end
    end

    def render_unauthorized(realm = "Application")
        self.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
        render json: 'Bad credentials', status: :unauthorized
    end
end

Bây giờ chúng ta có thể test API bằng lệnh curl như sau:

>> curl -H "Authorization: Token token=USER_TOKEN" http://localhost:3000/user

9. Document cho API

Có một vấn đề ở đây là khi chúng ta xây dựng một REST API server mà không có document cụ thể, thì sẽ gấy ra rất nhiều khó khăn cho các lập trình viên khác khi sử dụng API của chúng ta, cũng như khó khắn trong việc test API.Hiện nay có rất nhiều công cụ mã nguồn mở để giúp cho lập trình viên có thể viết document rõ ràng và có thể test endpoint trực tiếp mà k cần sử dụng đến các công cụ như postman để có thể test API. Hai mã nguồn phổ biến hiện nay là Swagger và API Blue Print, tuy nhiên nó khá là dài nên mình sẽ nói về hai công cụ đó ở bài viết sau, dươi đây là ví dụ về document API sử dụng Swagger mà mình dùng để viết cho project trên.

Như vậy chúng ta có thể thấy rằng để có thể tạo ra một API hoàn chỉnh thì chúng ta cần phải quan tâm đến các vấn đề quan trọng khác như là viết document, bảo vệ API của chúng ta trước các cuộc tấn công, .... Hy vọng bài viết giúp ích nhiều cho các bạn. See you later!

BuiQuangThang 22-07-2017

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Bùi Quang Thăng

2 bài viết.
0 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
2 0
Đề bài cho ta 2 file là create_folder_player.py và misc.py. Link đề bài: (Link) Trong trang chủ grandprix, thì yêu cầu chúng ta chạt netcat theo l...
Bùi Quang Thăng viết 1 năm trước
2 0
Bài viết liên quan
White
18 1
Toán tử XOR có tính chất: + A XOR A = 0 + 0 XOR A = A Với tính chất này, có thể cài đặt bài toán sau với độ phức tạp O(N) về runtime, và với O(1)...
kiennt viết hơn 1 năm trước
18 1
White
1 1
Chào mọi người, hôm nay mình viết một bài TIL nhỏ về cách lấy độ phân giải của màn hình hiện tại đang sử dụng. xdpyinfo | grep dimensions Kết quả...
namtx viết 7 tháng trước
1 1
White
8 0
Lấy fake path của file trong html input Ngữ cảnh: em cần làm một cái nút tải ảnh lên có preview. GIải pháp đầu: Dùng (Link) đọc file ảnh thành ba...
Hoàng Duy viết gần 2 năm trước
8 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
2 bài viết.
0 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!