Tạo JSON API với Rails 5
Bài đăng này đã không được cập nhật trong 8 năm
So với các phiên bản trước của Rails, Rails 5 đã được tích hợp gem rails-api
. Điều này giúp cho việc tạo API trong Rails dễ dàng hơn so với trước đây. Bài viết này sẽ hướng dẫn cách tạo JSON API với Rails 5 bằng tùy chọn --api
mới. Ngoài ra, tôi sẽ demo chức năng xác thực bằng một số tính năng mới trong Rails 5. Mã nguồn bài viết trên GitHub.
Rails 5 API
Khi tạo ứng dụng mới trong Rails 5, sẽ có thêm tùy chọn cờ --api
. Nó sẽ tạo ra ứng dụng Rails nhẹ hơn, chỉ phục vụ cho dữ liệu API. Trước đây, cần dùng gem rails-api
để làm điều này.
JSON:API
JSON:API sẽ định nghĩa cách server trả về dữ liệu dạng JSON, cách client nhận dữ liệu, thực hiện lọc, sắp xếp, phân trang, xử lý lỗi, mối quan hệ giữa các dữ liệu, mã trạng thái HTTP trả về và những thứ khác. Các thông số kỹ thuật này được định nghĩa trong JSON format chuẩn của Rails 5, tuy nhiên bạn có thể tùy biến nó.
Ứng dụng
Để show được hết các tính năng được mô tả bên trên, tôi sẽ tạo một ứng dụng dạng "blog", cho phép người dùng có thể đăng bài viết. Để đơn giản, sẽ chỉ có chức năng tạo, xác thực người dùng và đăng bài, không có đổi mật khẩu, phân quyền hay comment cho từng bài.
Cài đặt Rails 5 và tạo ứng dụng API
Để sử dụng được Rails 5, bạn phải dùng phiên bản Ruby 2.2.2
trở lên.
$ gem install rails
$ rails new rails5_json_api_demo --api
Các tùy chọn khác bạn có thể xem chi tiết với lệnh rails -h
. Ví dụ, sử dụng -C
để bỏ qua ActionCable (web sockets), -M
để bỏ qua ActionMailer, ...
Sử dụng CORS
CORS là cơ chế cho phép hạn chế nguồn tài nguyên trên một trang web từ những domain bên ngoài nguồn tài nguyên chuẩn.
gem rack-cors
$ bundle install
Để sử dụng CORS, cần tùy chỉnh file config/initializers/cors.rb
(Tham khảo rack-cors
GitHub repo). Ứng dụng này sử dụng tùy chọn mặc định. Tôi sẽ bỏ example.com
và thay thế bằng *
, trong trường hợp này không cần quan tâm request đến từ đâu.
Có thể dùng JSON-P thay thế cho CORS, tuy nhiên tôi sẽ không dùng nó ở đây. CORS được xác nhận bởi Rails và được W3C đề nghị, và tôi muốn sử dụng Rails mặc định. Ngoài ra, JSON-P
chỉ hỗ trợ phương thức GET
, không đủ cho ứng dụng này.
Serialization
Rails có cung cấp JSON serialization
, tuy nhiên tôi sẽ dùng gem active_model_serializers. Gem này cung cấp JsonApi Adapter
, sẽ tiết kiệm rất nhiều thời gian.
gem 'active_model_serializers', '~> 0.10.0'
Chạy bundle install
, sau đó tạo file config/initializers/active_model_serializers.rb
và thêm nội dung sau:
ActiveModel::Serializer.config.adapter = :json_api
Trong thư mục config/environments/
, thêm config sau vào 2 file development.rb
và test.rb
:
Rails.application.routes.default_url_options = {
host: 'localhost',
port: 3000
}
Tạo User
Xác thực là tính năng quan trong khi tạo user. Trong ứng dụng này, user chỉ có quyền tạo hoặc chỉnh sửa bài viết. Tôi sẽ sử dụng phương thức has_secure_token trong Rails 5. Bạn có thể dùng gem Devise hoặc JWT để thực hiện xác thực. Tuy nhiên, tôi cần sự đơn giản và cũng đang muốn tìm hiểu phương thức has_secure_token
mới này.
Bắt đầu migrate dữ liệu, chạy lệnh sau :
$ rails g migration CreateUsers
File được tạo ra có dạng như sau:
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.timestamps
t.string :full_name
t.string :password_digest
t.string :token
t.text :description
end
add_index :users, :token, unique: true
end
end
Một thay đổi trong Rails 5 với việc migrate dữ liệu. Bạn sẽ migrate với lệnh rails
chứ không phải với rake
như trước nữa. Đơn giản là để tạo bảng trong cơ sở dữ liệu, chạy lệnh rails db:migrate
.
Tạo model app/models/user.rb
:
class User < ApplicationRecord
has_secure_token
has_secure_password
validates :full_name, presence: true
end
Thêm route:
Rails.application.routes.draw do
resources :users
end
Khi thêm resource vào route trong ứng dụng Rails sử dụng cờ --api
, route cho new
và edit
không được tạo ra. Đây chính là điều chúng ta cần.
Tiếp theo, tạo serializer
cho model. Tạo file app/serializers/user_serializer.rb
:
class UserSerializer < ActiveModel::Serializer
attributes :id, :full_name, :description, :created_at
end
Chỉ cần những thông tin id
, full_name
, created_at
và description
là đủ.
Bước cuối là tạo users controller. Tạo file app/controllers/users_controller.rb
:
class UsersController < ApplicationController
def index
users = User.all
render json: users
end
end
Như bạn thấy, ActiveModel::Serializers
được tích hợp đầy đủ trong Rails controller.
Tạo 1 user mới trong Rails console (rails c
):
User.create(full_name: "Sasa J", password: "Test")
Khởi động server (rails s
) và chạy http://localhost:3000/users
trên trình duyệt, bạn sẽ thấy:
{"data":[
{"id":"1",
"type":"users",
"attributes":{
"full-name":"Sasa J",
"description":null,
"created-at":"2016-06-16T09:55:37.856Z"
}
}
]}
Đây chính là dữ liệu đầu ra chúng ta cần. Không những vậy, gem active_model_serializers
đã chuẩn hóa dữ liệu bằng cách thêm type
, tách id
và những dữ liệu còn lại, nó cũng thay thế gạch dưới _
bằng gạch ngang -
theo chuẩn dữ liệu JSON.
Loại phương tiện
Mở công cụ phát triển trong trình duyệt web của bạn, kiểm tra Content-Type
phản hồi từ máy chủ, bạn sẽ thấy application/json
. JSON:API yêu cầu sử dụng application/vnd.api+json
. Để thay đổi, mở config/initializers/mime_types.rb
và thêm đoạn code sau:
Mime::Type.register "application/vnd.api+json", :json
Đã giải quyết được vấn đề. Nếu dùng Firefox, bạn có thể sẽ nhận được hộp thoại nhắc tải về file users
. Để khắc phục, bạn thêm loại phương tiện này vào Firefox hoặc đơn giản là sử dụng trình duyệt hiểu được application/vnd.api+json
như Chrome. Tốt nhất, bạn nên dùng extension cho trình duyệt (Postman cho Chrome hoặc RESTED cho Firefox).
Tạo post
$ rails g migration CreatePosts
class CreatePosts < ActiveRecord::Migration[5.0]
def change
create_table :posts do |t|
t.timestamps
t.string :title
t.text :content
t.integer :user_id
t.string :category
t.integer :rating
end
end
end
Chạy rails db:migrate
và tạo model app/models/post.rb
:
class Post < ApplicationRecord
belongs_to :user
end
Đừng quên thêm has_many :posts, dependent: :destroy
vào user.rb
model!
Thêm resources :posts
vào file config/routes.rb
.
Tiếp đến, tạo serializer
cho post
. Tạo file app/serializers/post_serializer.rb
:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content, :category,
:rating, :created_at, :updated_at
belongs_to :user
end
ActiveModel::Serializer
dùng chung cách để mô tả mối quan hệ giữa các dữ liệu như ActiveRecord
, và nhớ thêm has_many :posts
vào UserSerializer!
Nếu bạn tạo dữ liệu cho post và check lại /user/
URL:
{"data":[{
"id":"1",
"type":"users",
"attributes":{
"full-name":"Sasa J",
"description":null,
"created-at":"2016-06-16T09:55:37.856Z"
},
"relationships":{
"posts":{
"data":[{
"id":"1",
"type":"posts"
}]
}
}
}]
}
Như bạn thấy, quan hệ giữa các dữ liệu là 1 phần của JSON:API
và ActiveModel::Serializer
làm điều này rất tốt.
Liên kết dữ liệu JSON
JSON:API
cho phép links
đến các JSON object
. Phía client có thể dùng để lấy nhiều dữ liệu hơn:
class UserSerializer < ActiveModel::Serializer
attributes :id, :full_name, :description, :created_at
has_many :posts
link(:self) { user_url(object) }
end
Check /users/
URL, bạn sẽ thấy links
block.
Tạo dữ liệu
Thông thường, phải viết test trước, tuy nhiên để giải thích một số khái niệm của Rails 5 và JSON:API trước khi đi vào TDD, tôi sẽ làm cho mọi thứ hoạt động trước khi test. Tôi thích sử dụng Minitest hơn RSpec, nó nhanh và đơn giản. Tạo dữ liệu:
# users.yml
<% 6.times do |i| %>
user_<%= i %>:
full_name: <%= "User Nr#{i}" %>
password_digest: <%= BCrypt::Password.create('password') %>
token: <%= SecureRandom.base58(24) %>
<% end %>
# posts.yml
<% 6.times do |i| %>
<% 25.times do |n| %>
article_<%= i %>_<%= n %>:
title: <%= "Example title #{i}/#{n}" %>
content: <%= "Example content #{i}/#{n}" %>
user: <%= "user_#{i}" %>
rating: <%= 1 + i + rand(3) %>
category: <%= i == 0 ? 'First' : 'Example' %>
<% end %>
<% end %>
Ta sẽ có 6 user và 150 post, mỗi user sẽ có 25 post. Tôi thêm biến rating
và category
để test chức năng lọc và phân loại.
Thêm test cho action index và show trong users controller
Thêm code vào test/controllers/users_controller_test.rb:
:
require 'test_helper'
require 'json'
class UsersControllerTest < ActionController::TestCase
test "Should get valid list of users" do
get :index
assert_response :success
assert_equal response.content_type, 'application/vnd.api+json'
jdata = JSON.parse response.body
assert_equal 6, jdata['data'].length
assert_equal jdata['data'][0]['type'], 'users'
end
test "Should get valid user data" do
user = users('user_1')
get :show, params: { id: user.id }
assert_response :success
jdata = JSON.parse response.body
assert_equal user.id.to_s, jdata['data']['id']
assert_equal user.full_name, jdata['data']['attributes']['full-name']
assert_equal user_url(user, { host: "localhost", port: 3000 }),
jdata['data']['links']['self']
end
test "Should get JSON:API error block when requesting user data with invalid ID" do
get :show, params: { id: "z" }
assert_response 404
jdata = JSON.parse response.body
assert_equal "Wrong ID provided", jdata['errors'][0]['detail']
assert_equal '/data/attributes/id', jdata['errors'][0]['source']['pointer']
end
end
Điều quan trọng nhất khi test là phải biết được cần test gì:
- Header
Content-Type
được set chung cho tất cả các trả về nên chỉ cần kiểm tra 1 lần là đủ. - Khi lấy danh sách thì chỉ cần quan tâm đến số lượng user mà không cần để ý đến chi tiết thông tin từng user.
- Khi lấy 1 user thì quan tâm đến
id
vàfull_name
, nếu nó đúng, phần còn lại đương nhiên đúng. Link
là phần được thêm vào nên tất nhiên cần test nó.default_url_options
trongconfig/environments/test.rb
làm việc với HTTP request mà không phải URL. Vì vậy, cần fix cứng host và port trong phần test. Trong user controller:
class UsersController < ApplicationController
before_action :set_user, only: [:show, :update, :destroy]
def index
users = User.all
render json: users
end
def show
render json: @user
end
private
def set_user
begin
@user = User.find params[:id]
rescue ActiveRecord::RecordNotFound
user = User.new
user.errors.add(:id, "Wrong ID provided")
render_error(user, 404) and return
end
end
end
Render lỗi trong ApplicationController:
class ApplicationController < ActionController::API
private
def render_error(resource, status)
render json: resource, status: status, adapter: :json_api,
serializer: ActiveModel::Serializer::ErrorSerializer
end
end
Chi tiết về error xem thêm tại active_model_serializer JSON:API errors document và errors part of JSON:API spec.
Tạo user
- Trước khi tạo hay sửa user, cần xác thực. Như đã nói bên trên, tôi dùng token xác thực.
- Khi gửi dữ liệu JSON đến server, cần set
Content-Type
header. Nếu dùng HTTP mà không gửi bất kỳ dữ liệu JSON nào (GET, DELETE) thì không cần setContent-Type
header. type
bắt buộc phải có trong dữ liệu JSON.
test "Creating new user without sending correct content-type should result in error" do
post :create, params: {}
assert_response 406
end
test "Creating new user without sending X-Api-Key should result in error" do
@request.headers["Content-Type"] = 'application/vnd.api+json'
post :create, params: {}
assert_response 403
end
test "Creating new user with incorrect X-Api-Key should result in error" do
@request.headers["Content-Type"] = 'application/vnd.api+json'
@request.headers["X-Api-Key"] = '0000'
post :create, params: {}
assert_response 403
end
test "Creating new user with invalid type in JSON data should result in error" do
user = users('user_1')
@request.headers["Content-Type"] = 'application/vnd.api+json'
@request.headers["X-Api-Key"] = user.token
post :create, params: { data: { type: 'posts' }}
assert_response 409
end
test "Creating new user with invalid data should result in error" do
user = users('user_1')
@request.headers["Content-Type"] = 'application/vnd.api+json'
@request.headers["X-Api-Key"] = user.token
post :create, params: {
data: {
type: 'users',
attributes: {
full_name: nil,
password: nil,
password_confirmation: nil }}}
assert_response 422
jdata = JSON.parse response.body
pointers = jdata['errors'].collect { |e|
e['source']['pointer'].split('/').last
}.sort
assert_equal ['full-name','password'], pointers
end
test "Creating new user with valid data should create new user" do
user = users('user_1')
@request.headers["Content-Type"] = 'application/vnd.api+json'
@request.headers["X-Api-Key"] = user.token
post :create, params: {
data: {
type: 'users',
attributes: {
full_name: 'User Number7',
password: 'password',
password_confirmation: 'password' }}}
assert_response 201
jdata = JSON.parse response.body
assert_equal 'User Number7',
jdata['data']['attributes']['full-name']
end
Thêm vào UsersController:
before_action :validate_user, only: [:create, :update, :destroy]
before_action :validate_type, only: [:create, :update]
def create
user = User.new(user_params)
if user.save
render json: user, status: :created
else
render_error(user, :unprocessable_entity)
end
end
private
def user_params
ActiveModelSerializers::Deserialization.jsonapi_parse(params)
end
end
Trong ApplicationController:
class ApplicationController < ActionController::API
before_action :check_header
private
def check_header
if ['POST','PUT','PATCH'].include? request.method
if request.content_type != "application/vnd.api+json"
head 406 and return
end
end
end
def validate_type
if params['data'] && params['data']['type']
if params['data']['type'] == params[:controller]
return true
end
end
head 409 and return
end
def validate_user
token = request.headers["X-Api-Key"]
head 403 and return unless token
user = User.find_by token: token
head 403 and return unless user
end
def render_error(resource, status)
render json: resource, status: status, adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer
end
end
Sửa user
Tương tự như khi tạo user, chỉ cần thêm test khi cập nhật dữ liệu thành công:
test "Updating an existing user with valid data should update that user" do
user = users('user_1')
@request.headers["Content-Type"] = 'application/vnd.api+json'
@request.headers["X-Api-Key"] = user.token
patch :update, params: {
id: user.id,
data: {
id: user.id,
type: 'users',
attributes: { full_name: 'User Number1a' }}}
assert_response 200
jdata = JSON.parse response.body
assert_equal 'User Number1a', jdata['data']['attributes']['full-name']
end
Trong UsersController:
def update
if @user.update_attributes(user_params)
render json: @user, status: :ok
else
render_error(@user, :unprocessable_entity)
end
end
Xóa user
Đơn giản, chỉ cần xóa dữ liệu và trả ra trạng thái 204 'Không có dữ liệu'
:
test "Should delete user" do
user = users('user_1')
ucount = User.count - 1
@request.headers["X-Api-Key"] = user.token
delete :destroy, params: { id: users('user_5').id }
assert_response 204
assert_equal ucount, User.count
end
destroy action trong UsersController:
def destroy
@user.destroy
head 204
end
Post
Tương tự cách làm như với user, trong phần post này tôi chú trọng đến phần index
vì đây là nơi thích hợp để thực hiện chức năng lọc, sắp xếp.
Trong test/controllers/posts_controller_test.rb
:
require 'test_helper'
require 'json'
class PostsControllerTest < ActionController::TestCase
test "Should get valid list of posts" do
get :index
assert_response :success
jdata = JSON.parse response.body
assert_equal Post.count, jdata['data'].length
assert_equal jdata['data'][0]['type'], 'posts'
end
end
Và app/controllers/posts_controller.rb
:
class PostsController < ApplicationController
def index
posts = Post.all
render json: posts
end
end
Phân trang
Có thể sử dụng gem kaminari
hoặc will_paginate
để phân trang. Tôi dùng will_paginate
.
Thêm gem "will_paginate"
trong Gemfile
và chạy bundle install
.
Sửa lại Post model:
class Post < ApplicationRecord
belongs_to :user
self.per_page = 50
end
Sau đó thêm phân trang vào PostsController:
def index
posts = Post.page(params[:page] ? params[:page][:number] : 1)
render json: posts
end
Khi đó sẽ có lỗi:
Failure:
PostsControllerTest#test_Should_get_valid_list_of_posts [<filename-removed>:10]:
Expected: 150
Actual: 50
Sửa lại file test:
test "Should get valid list of posts" do
get :index, params: { page: { number: 2 } }
assert_response :success
jdata = JSON.parse response.body
assert_equal Post.per_page, jdata['data'].length
assert_equal jdata['data'][0]['type'], 'posts'
end
Khi phân trang thì có thêm các chức năng như first
, prev
, next
và last
:
test "Should get valid list of posts" do
get :index, params: { page: { number: 2 } }
assert_response :success
jdata = JSON.parse response.body
assert_equal Post.per_page, jdata['data'].length
assert_equal jdata['data'][0]['type'], 'posts'
l = jdata['links']
assert_equal l['first'], l['prev']
assert_equal l['last'], l['next']
end
Tôi dùng tham số page
và JSON:API không quy định bắt buộc cho việc đặt tên này. Tuy nhiên nó dễ hiểu khi sử dụng page[number]
và page[size]
.
Ngoài ra, cần thêm đối tượng meta
để thêm các thông tin về phân trang như tổng số trang, số bản ghi:
class PostsController < ApplicationController
def index
posts = Post.page(params[:page] ? params[:page][:number] : 1)
render json: posts, meta: pagination_meta(posts)
end
private
def pagination_meta(object)
{
current_page: object.current_page,
next_page: object.next_page,
prev_page: object.previous_page,
total_pages: object.total_pages,
total_count: object.total_entries
}
end
end
Và trong file test, thêm assert
:
assert_equal Post.count, jdata['meta']['total-count']
Thêm meta key
Có thể thêm nhiều thông tin hơn cho JSON message sử dụng meta
key. Ví dụ như phiên bản API, ngày cập nhật cuối cùng, thông tin bản quyền ... Thêm vào ApplicationController:
def default_meta
{
licence: 'CC-0',
authors: ['Saša']
}
end
Khi sử dụng, thay render json: object
bằng render json: object, meta: default_meta
. Khi phân trang có thể thêm pagination_meta(posts).merge(default_meta)
.
Thêm tài nguyên
Đôi khi bạn cần thêm thông tin vào dữ liệu JSON trả ra, ví dụ khi lấy dữ liệu về post
bạn cần thêm thông tin về tác giả. Thay vì phải thêm mới dữ liệu, có thể dùng include
:
render json: posts, meta: pagination_meta(posts), include: ['user']
Nó sẽ thêm đối tượng included
trong JSON, sau đối tượng data
.
Sắp xếp
Để sử dụng chức năng sắp xếp với JSON:API, cần dùng tham số sort
. Có thể sắp xếp cùng lúc nhiều trường. Mặc định luôn là sắp xếp tăng dần, trừ khi có thêm tùy chọn cụ thể, sử dụng -
. Ví dụ, /posts?sort=-rating
trả về danh sách bản ghi có lượng rating từ cao xuống thấp.
Trong file test:
test "Should get properly sorted list" do
post = Post.order('rating DESC').first
get :index, params: { sort: '-rating' }
assert_response :success
jdata = JSON.parse response.body
assert_equal post.title, jdata['data'][0]['attributes']['title']
end
Trong file controller:
def index
posts = Post.all
if params['sort']
f = params['sort'].split(',').first
field = f[0] == '-' ? f[1..-1] : f
order = f[0] == '-' ? 'DESC' : 'ASC'
if Post.new.has_attribute?(field)
posts = posts.order("#{field} #{order}")
end
end
posts = posts.page(params[:page] ? params[:page][:number] : 1)
render json: posts, meta: pagination_meta(posts), include: ['user']
end
Lọc
test "Should get filtered list" do
get :index, params: { filter: 'First' }
assert_response :success
jdata = JSON.parse response.body
assert_equal Post.where(category: 'First').count, jdata['data'].length
end
Trong PostsController:
def index
posts = Post.all
if params[:filter]
posts = posts.where(["category = ?", params[:filter]])
end
if params['sort']
f = params['sort'].split(',').first
field = f[0] == '-' ? f[1..-1] : f
order = f[0] == '-' ? 'DESC' : 'ASC'
if Post.new.has_attribute?(field)
posts = posts.order("#{field} #{order}")
end
end
posts = posts.page(params[:page] ? params[:page][:number] : 1)
render json: posts, meta: pagination_meta(posts), include: ['user']
end
Đăng nhập, đăng xuất
Khi cần tạo hay thay đổi tài nguyên, sử dụng token X-Api-Key
. Nhưng làm thế nào để Client/SPA
nhận biết được nên làm điều gì trước, cần thêm session
:
test/controllers/sessions_routes_test.rb
:
require 'test_helper'
class SessionsRoutesTest < ActionController::TestCase
test "should route to create session" do
assert_routing({ method: 'post', path: '/sessions' },
{ controller: "sessions", action: "create" })
end
test "should route to delete session" do
assert_routing({ method: 'delete', path: '/sessions/something'},
{ controller: "sessions", action: "destroy", id: "something" })
end
end
Trong config/routes.rb
:
post 'sessions' => 'sessions#create'
delete 'sessions/:id' => 'sessions#destroy'
app/serializers/session_serializer.rb
:
class SessionSerializer < ActiveModel::Serializer
attributes :id, :full_name, :token
end
test/controllers/sessions_controller_test.rb
:
require 'test_helper'
require 'json'
class SessionsControllerTest < ActionController::TestCase
test "Creating new session with valid data should create new session" do
user = users('user_0')
@request.headers["Content-Type"] = 'application/vnd.api+json'
post :create, params: {
data: {
type: 'sessions',
attributes: {
full_name: user.full_name,
password: 'password' }}}
assert_response 201
jdata = JSON.parse response.body
refute_equal user.token, jdata['data']['attributes']['token']
end
test "Should delete session" do
user = users('user_0')
delete :destroy, params: { id: user.token }
assert_response 204
end
end
Và app/controllers/sessions_controller.rb
:
class SessionsController < ApplicationController
def create
data = ActiveModelSerializers::Deserialization.jsonapi_parse(params)
Rails.logger.error params.to_yaml
user = User.where(full_name: data[:full_name]).first
head 406 and return unless user
if user.authenticate(data[:password])
user.regenerate_token
render json: user, status: :created, meta: default_meta,
serializer: ActiveModel::Serializer::SessionSerializer and return
end
head 403
end
def destroy
user = User.where(token: params[:id]).first
head 404 and return unless user
user.regenerate_token
head 204
end
end
- Sử dụng
SessionSerializer
để tuần tự khi đăng nhập. - Để đảm bảo an toàn mỗi khi đăng nhập đăng xuất, tôi reset token. Đây là 1 phần của Rails 5.
- Khi dùng
render json: user
, client sẽ lấy được JSON tuần tự từUserSerializer
. Đó là lý do tôi sử dụngSessionSerializer
. - Có 1 class trong Rails có tên là
SessionSerializer
nên có vẻ như sử dụng tên không hợp lý. Tuy nhiên, Session middleware đã bị vô hiệu hóa khi sử dụng--api
.
Hiệu lực
Để bảo mật hơn, nên sử dụng timeouts cho mỗi phiên hoạt động. Nếu không truy cập vào bất kỳ API nào trên server thì sẽ tự động đăng xuất. Ngoài ra sẽ dùng khóa meta
để thông báo tình trạng đăng nhập cho client.
Đầu tiên là thay đổi updated_at
:
<% 6.times do |i| %>
user_<%= i %>:
full_name: <%= "User Nr#{i}" %>
password_digest: <%= BCrypt::Password.create('password') %>
token: <%= SecureRandom.base58(24) %>
updated_at: <%= i == 5 ? 1.hour.ago : Time.now %>
<% end %>
Thay đổi ApplicationController:
before_action :validate_login
def validate_login
token = request.headers["X-Api-Key"]
return unless token
user = User.find_by token: token
return unless user
if 15.minutes.ago < user.updated_at
user.touch
@current_user = user
end
end
def validate_user
head 403 and return unless @current_user
end
def default_meta
{
licence: 'CC-0',
authors: ['Saša'],
logged_in: (@current_user ? true : false)
}
end
before_action
luôn được gọi trước mỗi yêu cầu và sẽ check hoạt động trong 15 phút gần nhất của user.- Đặt biến toàn cục
@current_user
. validate_user
được thay đổi liên tục.default_meta
mô tả thông tin đăng nhập. Trong file test:
require 'test_helper'
require 'json'
class SessionFlowTestTest < ActionDispatch::IntegrationTest
test "login timeout and meta/logged-in key test" do
user = users('user_5')
# Not logged in, because of timeout
get '/users', params: nil,
headers: { 'X-Api-Key' => user.token }
assert_response :success
jdata = JSON.parse response.body
assert_equal false, jdata['meta']['logged-in']
# Log in
post '/sessions',
params: {
data: {
type: 'sessions',
attributes: {
full_name: user.full_name,
password: 'password' }}}.to_json,
headers: { 'Content-Type' => 'application/vnd.api+json' }
assert_response 201
jdata = JSON.parse response.body
token = jdata['data']['attributes']['token']
refute_equal user.token, token
# Logged in
get '/users', params: nil,
headers: { 'X-Api-Key' => token }
assert_response :success
jdata = JSON.parse response.body
assert_equal true, jdata['meta']['logged-in']
end
end
Kết luận
Tùy chọn --api
thực sự đã tạo ra 1 ứng dụng nhẹ với nhiều chức năng. Có chút đặc biệt khi sử dụng --api
là đầu tiên Rails sẽ tạo ứng dụng đầy đủ, sau đó loại bỏ các tập tin không cần thiết để còn lại ứng dụng API duy nhất. Thậm chí có thể xóa nhiều tập tin hơn nếu không sử dụng ActionCable (-C
).
Rails mặc định sử dụng dạng JSON mặc định của nó, không có gì là áp đặt, nhưng tôi thích nhiều thứ hơn là dạng chuẩn đó với các thông tin rõ ràng hơn. JSON:API là lựa chọn tốt nhất, khi đó gem active_model_serializers
thực sự hữu dụng.
Mã nguồn ứng dụng hiện có trên GitHub.
Nguồn: Creating Rails 5 API only application following JSON:API specification.
All rights reserved