Computer >> Máy Tính >  >> Lập trình >> Ruby

Chế tạo API với đường ray

Ngày nay, một thực tế phổ biến là phụ thuộc nhiều vào API (giao diện lập trình ứng dụng). Không chỉ các dịch vụ lớn như Facebook và Twitter mới sử dụng chúng — API rất phổ biến do sự phổ biến của các khuôn khổ phía máy khách như React, Angular và nhiều framework khác. Ruby on Rails đang theo xu hướng này và phiên bản mới nhất trình bày một tính năng mới cho phép bạn tạo các ứng dụng chỉ API.

Ban đầu, chức năng này được đóng gói trong một viên ngọc riêng có tên là rails-api, nhưng kể từ khi phát hành Rails 5, nó hiện là một phần cốt lõi của khung công tác. Tính năng này cùng với ActionCable có lẽ được mong đợi nhất và vì vậy hôm nay chúng ta sẽ thảo luận về nó.

Bài viết này trình bày cách tạo các ứng dụng Rails chỉ có API và giải thích cách cấu trúc các tuyến đường và bộ điều khiển của bạn, phản hồi với định dạng JSON, thêm trình tuần tự và thiết lập CORS (Chia sẻ tài nguyên chéo). Bạn cũng sẽ tìm hiểu về một số tùy chọn để bảo mật API và bảo vệ nó khỏi bị lạm dụng.

Nguồn cho bài viết này có tại GitHub.

Tạo ứng dụng chỉ API

Để bắt đầu, hãy chạy lệnh sau:

rails new RailsApiDemo --api

Nó sẽ tạo một ứng dụng Rails chỉ API mới có tên là RailsApiDemo . Đừng quên rằng hỗ trợ cho --api tùy chọn chỉ được thêm vào Rails 5, vì vậy hãy đảm bảo rằng bạn đã cài đặt tùy chọn này hoặc phiên bản mới hơn.

Mở Gemfile và lưu ý rằng nó nhỏ hơn nhiều so với bình thường:gems như coffee-rails , turbolinkssass-rails đã biến mất.

config / application.rb tệp chứa một dòng mới:

config.api_only = true

Điều đó có nghĩa là Rails sẽ tải một tập hợp phần mềm trung gian nhỏ hơn:ví dụ:không có cookie và hỗ trợ phiên. Hơn nữa, nếu bạn cố gắng tạo một giàn giáo, các chế độ xem và nội dung sẽ không được tạo. Trên thực tế, nếu bạn kiểm tra chế độ xem / bố cục , bạn sẽ lưu ý rằng application.html.erb tệp cũng bị thiếu.

Một sự khác biệt quan trọng khác là ApplicationController kế thừa từ ActionController::API , không phải ActionController::Base .

Đó là khá nhiều - nói chung, đây là một ứng dụng Rails cơ bản mà bạn đã thấy nhiều lần. Bây giờ, hãy thêm một vài mô hình để chúng ta có thứ gì đó để làm việc:

rails g model User name:string
rails g model Post title:string body:text user:belongs_to
rails db:migrate

Không có gì lạ mắt đang xảy ra ở đây:một bài đăng có tiêu đề và nội dung thuộc về người dùng.

Đảm bảo rằng các liên kết thích hợp được thiết lập và cũng cung cấp một số kiểm tra xác thực đơn giản:

models / user.rb

  has_many :posts

  validates :name, presence: true

models / post.rb

  belongs_to :user

  validates :title, presence: true
  validates :body, presence: true

Rực rỡ! Bước tiếp theo là tải một vài bản ghi mẫu vào các bảng mới được tạo.

Đang tải dữ liệu demo

Cách dễ nhất để tải một số dữ liệu là sử dụng hạt giống.rb tệp bên trong db danh mục. Tuy nhiên, tôi lười (cũng như nhiều lập trình viên khác) và không muốn nghĩ ra bất kỳ nội dung mẫu nào. Do đó, tại sao chúng ta không tận dụng faker gem có thể tạo ra dữ liệu ngẫu nhiên thuộc nhiều loại:tên, email, từ hipster, văn bản "lorem ipsum", v.v.

Gemfile

group :development do
    gem 'faker'
end

Cài đặt đá quý:

bundle install

Bây giờ hãy tinh chỉnh hạt giống.rb :

db / seed.rb

5.times do
  user = User.create({name: Faker::Name.name})
  user.posts.create({title: Faker::Book.title, body: Faker::Lorem.sentence})
end

Cuối cùng, tải dữ liệu của bạn:

rails db:seed

Trả lời bằng JSON

Tất nhiên, bây giờ chúng ta cần một số tuyến đường và bộ điều khiển để tạo ra API của chúng ta. Một thực tế phổ biến là lồng các tuyến của API dưới api/ đường dẫn. Ngoài ra, các nhà phát triển thường cung cấp phiên bản của API trong đường dẫn, ví dụ:api/v1/ . Sau đó, nếu một số thay đổi đột phá phải được giới thiệu, bạn có thể chỉ cần tạo một không gian tên mới (v2 ) và một bộ điều khiển riêng biệt.

Đây là cách các tuyến đường của bạn có thể trông:

config / route.rb

namespace 'api' do
    namespace 'v1' do
      resources :posts
      resources :users
    end
end

Điều này tạo ra các tuyến như:

api_v1_posts GET    /api/v1/posts(.:format)     api/v1/posts#index
             POST   /api/v1/posts(.:format)     api/v1/posts#create
 api_v1_post GET    /api/v1/posts/:id(.:format) api/v1/posts#show

Bạn có thể sử dụng scope thay vì namespace , nhưng theo mặc định, nó sẽ tìm kiếm UsersControllerPostsController bên trong bộ điều khiển thư mục, không bên trong bộ điều khiển / api / v1 , vì vậy hãy cẩn thận.

Tạo api thư mục có thư mục lồng nhau v1 bên trong bộ điều khiển . Điền nó vào bộ điều khiển của bạn:

bộ điều khiển / api / v1 / users_controller.rb

module Api
    module V1
        class UsersController < ApplicationController
        end
    end
end

bộ điều khiển / api / v1 / posts_controller.rb

module Api
    module V1
        class PostsController < ApplicationController
        end
    end
end

Lưu ý rằng bạn không chỉ phải lồng tệp của bộ điều khiển trong api / v1 đường dẫn, nhưng bản thân lớp cũng phải được đặt tên bên trong ApiV1 mô-đun.

Câu hỏi tiếp theo là làm thế nào để phản hồi đúng với dữ liệu có định dạng JSON? Trong bài viết này, chúng tôi sẽ thử các giải pháp sau:đá quý jBuilder và active_model_serializers. Vì vậy, trước khi tiếp tục phần tiếp theo, hãy thả chúng vào Gemfile :

Gemfile

gem 'jbuilder', '~> 2.5'
gem 'active_model_serializers', '~> 0.10.0'

Sau đó chạy:

bundle install

Sử dụng jBuilder Gem

jBuilder là một gem phổ biến được duy trì bởi nhóm Rails, cung cấp một DSL đơn giản (ngôn ngữ dành riêng cho miền) cho phép bạn xác định cấu trúc JSON trong chế độ xem của mình.

Giả sử chúng tôi muốn hiển thị tất cả các bài đăng khi người dùng truy cập vào index hành động:

bộ điều khiển / api / v1 / posts_controller.rb

 def index
    @posts = Post.order('created_at DESC')
end

Tất cả những gì bạn cần làm là tạo chế độ xem được đặt tên theo hành động tương ứng với .json.jbuilder sự mở rộng. Lưu ý rằng chế độ xem phải được đặt dưới api / v1 cả đường dẫn:

views / api / v1 / posts / index.json.jbuilder

json.array! @posts do |post|
  json.id post.id
  json.title post.title
  json.body post.body
end

json.array! đi ngang qua @posts mảng. json.id , json.titlejson.body tạo các khóa có tên tương ứng đặt các đối số làm giá trị. Nếu bạn điều hướng đến https:// localhost:3000 / api / v1 / posts.json, bạn sẽ thấy một kết quả tương tự như sau:

[
    {"id": 1, "title": "Title 1", "body": "Body 1"},
    {"id": 2, "title": "Title 2", "body": "Body 2"}
]

Điều gì sẽ xảy ra nếu chúng tôi muốn hiển thị cả tác giả cho mỗi bài đăng? Thật đơn giản:

json.array! @posts do |post|
  json.id post.id
  json.title post.title
  json.body post.body
  json.user do
    json.id post.user.id
    json.name post.user.name
  end
end

Đầu ra sẽ thay đổi thành:

[
    {"id": 1, "title": "Title 1", "body": "Body 1", "user": {"id": 1, "name": "Username"}}
]

Nội dung của .jbuilder các tệp là mã Ruby thuần túy, vì vậy bạn có thể sử dụng tất cả các thao tác cơ bản như bình thường.

Lưu ý rằng jBuilder hỗ trợ các chia phần giống như bất kỳ chế độ xem Rails thông thường nào, vì vậy bạn cũng có thể nói:

json.partial! partial: 'posts/post', collection: @posts, as: :post

và sau đó tạo views / api / v1 / posts / _post.json.jbuilder tệp với các nội dung sau:

json.id post.id
json.title post.title
json.body post.body
json.user do
    json.id post.user.id
    json.name post.user.name
end

Vì vậy, như bạn thấy, jBuilder rất dễ dàng và thuận tiện. Tuy nhiên, để thay thế, bạn có thể dính vào các bộ tuần tự hóa, vì vậy chúng ta hãy thảo luận về chúng trong phần tiếp theo.

Sử dụng Serializers

Đá quý rails_model_serializers được tạo ra bởi một nhóm, những người ban đầu quản lý rails-api. Như đã nêu trong tài liệu, rails_model_serializers mang lại quy ước về cấu hình cho thế hệ JSON của bạn. Về cơ bản, bạn xác định trường nào sẽ được sử dụng khi tuần tự hóa (nghĩa là tạo JSON).

Đây là bộ tuần tự đầu tiên của chúng tôi:

serializers / post_serializer.rb

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :body
end

Ở đây chúng tôi nói rằng tất cả các trường này phải có trong JSON kết quả. Bây giờ các phương pháp như to_jsonas_json được gọi khi một bài đăng sẽ sử dụng cấu hình này và trả lại nội dung phù hợp.

Để xem nó hoạt động, hãy sửa đổi index hành động như thế này:

bộ điều khiển / api / v1 / posts_controller.rb

def index
    @posts = Post.order('created_at DESC')
    
    render json: @posts
end

as_json sẽ tự động được gọi khi @posts đối tượng.

Còn người dùng thì sao? Serializers cho phép bạn chỉ ra các mối quan hệ, giống như các mô hình. Hơn nữa, các bộ nối tiếp có thể được lồng vào nhau:

serializers / post_serializer.rb

Các thuộc tính
class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :body
  belongs_to :user

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

Bây giờ khi bạn tuần tự hóa bài đăng, nó sẽ tự động chứa user lồng nhau khóa với id và tên của nó. Nếu sau này bạn tạo một bộ tuần tự riêng cho người dùng với :id thuộc tính bị loại trừ:

serializers / post_serializer.rb

class UserSerializer < ActiveModel::Serializer
    attributes :name
end

rồi đến @user.as_json sẽ không trả lại id của người dùng. Tuy nhiên, @post.as_json sẽ trả về cả tên và id của người dùng, vì vậy hãy lưu ý.

Bảo mật API

Trong nhiều trường hợp, chúng tôi không muốn bất kỳ ai chỉ thực hiện bất kỳ hành động nào bằng cách sử dụng API. Vì vậy, hãy trình bày một quy trình kiểm tra bảo mật đơn giản và buộc người dùng của chúng tôi gửi mã thông báo của họ khi tạo và xóa bài đăng.

Mã thông báo sẽ có tuổi thọ không giới hạn và được tạo khi người dùng đăng ký. Trước hết, hãy thêm một token mới cột users bảng:

rails g migration add_token_to_users token:string:index

Chỉ mục này phải đảm bảo tính duy nhất vì không thể có hai người dùng có cùng mã thông báo:

db / migrate / xyz_add_token_to_users.rb

add_index :users, :token, unique: true

Áp dụng việc di chuyển:

rails db:migrate

Bây giờ hãy thêm before_save gọi lại:

models / user.rb

before_create -> {self.token = generate_token}

generate_token private method sẽ tạo mã thông báo trong một chu kỳ vô tận và kiểm tra xem nó có phải là duy nhất hay không. Ngay sau khi tìm thấy mã thông báo duy nhất, hãy trả lại:

models / user.rb

private

def generate_token
    loop do
      token = SecureRandom.hex
      return token unless User.exists?({token: token})
    end
end

Bạn có thể sử dụng một thuật toán khác để tạo mã thông báo, ví dụ:dựa trên băm MD5 của tên người dùng và một số muối.

Đăng ký Người dùng

Tất nhiên, chúng tôi cũng cần cho phép người dùng đăng ký, vì nếu không họ sẽ không thể lấy được mã thông báo của mình. Tôi không muốn giới thiệu bất kỳ chế độ xem HTML nào vào ứng dụng của chúng tôi, vì vậy thay vào đó, hãy thêm một phương thức API mới:

bộ điều khiển / api / v1 / users_controller.rb

def create
    @user = User.new(user_params)
    if @user.save
      render status: :created
    else
      render json: @user.errors, status: :unprocessable_entity
    end
end

private

def user_params
    params.require(:user).permit(:name)
end

Bạn nên trả về các mã trạng thái HTTP có ý nghĩa để các nhà phát triển hiểu chính xác những gì đang diễn ra. Giờ đây, bạn có thể cung cấp một bộ tuần tự mới cho người dùng hoặc gắn bó với một .json.jbuilder tập tin. Tôi thích biến thể thứ hai hơn (đó là lý do tại sao tôi không chuyển :json tùy chọn render ), nhưng bạn có thể tự do chọn bất kỳ phương pháp nào trong số chúng. Tuy nhiên, lưu ý rằng mã thông báo không được luôn được tuần tự hóa, chẳng hạn như khi bạn trả lại danh sách tất cả người dùng — danh sách này phải được giữ an toàn!

views / api / v1 / users / create.json.jbuilder

json.id @user.id
json.name @user.name
json.token @user.token

Bước tiếp theo là kiểm tra xem mọi thứ có hoạt động bình thường không. Bạn có thể sử dụng curl lệnh hoặc viết một số mã Ruby. Vì bài viết này nói về Ruby, tôi sẽ đi với tùy chọn mã hóa.

Kiểm tra đăng ký của người dùng

Để thực hiện một yêu cầu HTTP, chúng tôi sẽ sử dụng đá quý Faraday, cung cấp giao diện chung trên nhiều bộ ghi nhớ (mặc định là Net::HTTP ). Tạo một tệp Ruby riêng biệt, bao gồm Faraday và thiết lập máy khách:

api_client.rb

require 'faraday'

client = Faraday.new(url: 'https://localhost:3000') do |config|
  config.adapter  Faraday.default_adapter
end

response = client.post do |req|
  req.url '/api/v1/users'
  req.headers['Content-Type'] = 'application/json'
  req.body = '{ "user": {"name": "test user"} }'
end

Tất cả các tùy chọn này khá dễ hiểu:chúng tôi chọn bộ điều hợp mặc định, đặt URL yêu cầu thành https:// localhost:300 / api / v1 / users, thay đổi loại nội dung thành application/json và cung cấp nội dung yêu cầu của chúng tôi.

Phản hồi của máy chủ sẽ chứa JSON, vì vậy để phân tích cú pháp, tôi sẽ sử dụng Oj gem:

api_client.rb

require 'oj'

# client here...

puts Oj.load(response.body)
puts response.status

Ngoài phản hồi được phân tích cú pháp, tôi cũng hiển thị mã trạng thái cho mục đích gỡ lỗi.

Bây giờ bạn có thể chỉ cần chạy tập lệnh này:

ruby api_client.rb

và lưu trữ mã thông báo đã nhận ở đâu đó — chúng tôi sẽ sử dụng mã này trong phần tiếp theo.

Xác thực bằng mã thông báo

Để thực thi xác thực mã thông báo, authenticate_or_request_with_http_token phương pháp có thể được sử dụng. Nó là một phần của mô-đun ActionController ::HttpAuthentication ::Token ::ControllerMethods, vì vậy đừng quên bao gồm nó:

bộ điều khiển / api / v1 / posts_controller.rb

class PostsController < ApplicationController
    include ActionController::HttpAuthentication::Token::ControllerMethods
    # ...
end

Thêm một before_action mới và phương pháp tương ứng:

bộ điều khiển / api / v1 / posts_controller.rb

before_action :authenticate, only: [:create, :destroy]

# ...

private

# ...

def authenticate
    authenticate_or_request_with_http_token do |token, options|
      @user = User.find_by(token: token)
    end
end

Bây giờ, nếu mã thông báo không được đặt hoặc nếu không tìm thấy người dùng có mã thông báo như vậy, thì lỗi 401 sẽ được trả về, khiến hành động ngừng thực thi.

Xin lưu ý rằng giao tiếp giữa máy khách và máy chủ phải được thực hiện qua HTTPS, vì nếu không các mã thông báo có thể dễ dàng bị giả mạo. Tất nhiên, giải pháp được cung cấp không phải là lý tưởng và trong nhiều trường hợp, bạn nên sử dụng giao thức OAuth 2 để xác thực. Có ít nhất hai viên ngọc giúp đơn giản hóa quá trình hỗ trợ tính năng này một cách đáng kể:Doorkeeper và oPRO.

Tạo bài đăng

Để xem xác thực của chúng tôi đang hoạt động, hãy thêm create hành động với PostsController :

bộ điều khiển / api / v1 / posts_controller.rb

def create
    @post = @user.posts.new(post_params)
    if @post.save
        render json: @post, status: :created
    else
        render json: @post.errors, status: :unprocessable_entity
    end
end

Chúng tôi tận dụng bộ tuần tự hóa ở đây để hiển thị JSON thích hợp. @user đã được đặt bên trong before_action .

Bây giờ hãy kiểm tra mọi thứ bằng cách sử dụng mã đơn giản sau:

api_client.rb

client = Faraday.new(url: 'https://localhost:3000') do |config|
  config.adapter  Faraday.default_adapter
  config.token_auth('127a74dbec6f156401b236d6cb32db0d')
end

response = client.post do |req|
  req.url '/api/v1/posts'
  req.headers['Content-Type'] = 'application/json'
  req.body = '{ "post": {"title": "Title", "body": "Text"} }'
end

Thay thế đối số được truyền vào token_auth với mã thông báo nhận được khi đăng ký và chạy tập lệnh.

ruby api_client.rb

Xóa bài đăng

Việc xóa bài viết được thực hiện theo cách tương tự. Thêm destroy hành động:

bộ điều khiển / api / v1 / posts_controller.rb

def destroy
    @post = @user.posts.find_by(params[:id])
    if @post
      @post.destroy
    else
      render json: {post: "not found"}, status: :not_found
    end
end

Chúng tôi chỉ cho phép người dùng hủy các bài đăng mà họ thực sự sở hữu. Nếu bài viết được gỡ bỏ thành công, mã trạng thái 204 (không có nội dung) sẽ được trả lại. Ngoài ra, bạn có thể trả lời bằng id của bài đăng đã bị xóa vì nó sẽ vẫn còn trong bộ nhớ.

Đây là đoạn mã để kiểm tra tính năng mới này:

api_client.rb

response = client.delete do |req|
  req.url '/api/v1/posts/6'
  req.headers['Content-Type'] = 'application/json'
end

Thay thế id của bài đăng bằng một số phù hợp với bạn.

Thiết lập CORS

Nếu bạn muốn cho phép các dịch vụ web khác truy cập API của mình (từ phía máy khách), thì CORS (Chia sẻ tài nguyên nhiều nguồn gốc) phải được thiết lập đúng cách. Về cơ bản, CORS cho phép các ứng dụng web gửi các yêu cầu AJAX đến các dịch vụ của bên thứ ba. May mắn thay, có một viên ngọc gọi là rack-cors cho phép chúng ta dễ dàng thiết lập mọi thứ. Thêm nó vào Gemfile :

Gemfile

gem 'rack-cors'

Cài đặt nó:

bundle install

Và sau đó cung cấp cấu hình bên trong config / initializers / cors.rb tập tin. Trên thực tế, tệp này đã được tạo cho bạn và chứa một ví dụ sử dụng. Bạn cũng có thể tìm thấy một số tài liệu khá chi tiết trên trang của gem.

Ví dụ:cấu hình sau sẽ cho phép bất kỳ ai truy cập API của bạn bằng bất kỳ phương pháp nào:

config / initializers / cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '/api/*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Ngăn chặn Lạm dụng

Điều cuối cùng tôi sẽ đề cập trong hướng dẫn này là cách bảo vệ API của bạn khỏi lạm dụng và tấn công từ chối dịch vụ. Có một loại đá quý hay được gọi là rack-attack (do những người từ Kickstarter tạo ra) cho phép bạn đưa vào danh sách đen hoặc danh sách trắng các máy khách, ngăn chặn sự tràn ngập của máy chủ với các yêu cầu và hơn thế nữa.

Thả đá quý vào Gemfile :

Gemfile

gem 'rack-attack'

Cài đặt nó:

bundle install

Và sau đó cung cấp cấu hình bên trong rack_attack.rb tập tin khởi tạo. Tài liệu của gem liệt kê tất cả các tùy chọn có sẵn và đề xuất một số trường hợp sử dụng. Đây là cấu hình mẫu hạn chế bất kỳ ai ngoại trừ bạn truy cập dịch vụ và giới hạn số lượng yêu cầu tối đa là 5 mỗi giây:

config / initializers / rack_attack.rb

class Rack::Attack
  safelist('allow from localhost') do |req|
    # Requests are allowed if the return value is truthy
    '127.0.0.1' == req.ip || '::1' == req.ip
  end

  throttle('req/ip', :limit => 5, :period => 1.second) do |req|
    req.ip
  end
end

Một điều khác cần được thực hiện là bao gồm RackAttack làm phần mềm trung gian:

config / application.rb

config.middleware.use Rack::Attack

Kết luận

Chúng ta đã đến phần cuối của bài viết này. Hy vọng rằng bây giờ bạn cảm thấy tự tin hơn về việc tạo API với Rails! Xin lưu ý rằng đây không phải là lựa chọn khả dụng duy nhất — một giải pháp phổ biến khác đã có từ khá lâu là khung Nho, vì vậy bạn cũng có thể quan tâm đến việc kiểm tra nó.

Đừng ngần ngại đăng câu hỏi của bạn nếu bạn có điều gì đó không rõ ràng. Tôi cảm ơn bạn đã ở lại với tôi và chúc bạn viết mã vui vẻ!