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
, turbolinks
và sass-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 UsersController
và PostsController
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 Api
và V1
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.title
và json.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_json
và as_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ínhclass 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ẻ!