rails 构建 API
我是來鼓吹使用 Rails 寫 API 的。
原文在此: https://labs.kollegorna.se/blog/2015/04/build-an-api-now/
原文有一個很大的缺陷就是讀者無法按照它的步驟一步一步的去實現一個可以運行的 demo, 這對經驗豐富的開發 者可能不算是一個問題,但是對剛剛接觸這方面知識的新手來說卻是一個很大的遺憾,軟件開發方面的知識重在 實踐,只有動手做了,才能對知識掌握地更加牢靠,才能更好地在工作中去使用這些知識, 所以我對原文的內容 做了一些補充修整,以期能夠讓讀者邊讀邊思考邊實踐。
原文的 demo 是一個類微博應用,為簡單起見我們只使用 User 和 Micropost 模型, 并且我們不使用 ActiveModel::Serializer, 而是使用 Jbuilder 作為 json 模版。
首先建立一個項目: build-an-api-rails-demo
$ rails new build-an-api-rails-demo
加入第一個 API resource
BaseController
生成控制器:
# 我們不需要生成資源文件
$ bundle exe rails g controller api/v1/base --no-assets
app/controllers/api/v1/base_controller.rb,
class Api::V1::BaseController < ApplicationController
? # disable the CSRF token
? protect_from_forgery with: :null_session
? # disable cookies (no set-cookies header in response)
? before_action :destroy_session
? # disable the CSRF token
? skip_before_action :verify_authenticity_token
? def destroy_session
??? request.session_options[:skip] = true
? end
end
在 BaseController 里我們禁止了 CSRF token 和 cookies
配置路由:
config/routes.rb,
namespace :api do
? namespace :v1 do
??? resources :users, only: [:index, :create, :show, :update, :destroy]
??? # 原文有 microposts, 我們現在把它注釋掉
??? # resources :microposts, only: [:index, :create, :show, :update, :destroy]
? end
end
Api::V1::UsersController
生成控制器:
# 我們不需要生成資源文件
$ bundle exe rails g controller api/v1/users --no-assets
app/controllers/api/v1/users_controller.rb,
class Api::V1::UsersController < Api::V1::BaseController
? def show
??? @user = User.find(params[:id])
??? # 原文使用 Api::V1::UserSerializer
??? # 我們現在使用 app/views/api/v1/users/show.json.jbuilder
??? # render(json: Api::V1::UserSerializer.new(user).to_json)
? end
end
app/views/api/v1/users/show.json.jbuilder,
json.user do
? json.(@user, :id, :email, :name,? :activated, :admin, :created_at, :updated_at)
end
User 模型和 users 表
$ bundle exe rails g model User
app/models/user.rb,
class User < ActiveRecord::Base
end
db/migrate/20150502072954_create_users.rb,
class CreateUsers < ActiveRecord::Migration
? def change
??? create_table :users do |t|
????? t.string :email
????? t.string :name
????? t.datetime :activated
????? t.boolean :admin, default: false
????? t.timestamps null: false
??? end
? end
end
數據遷移:
$ bundle exe rake db:migrate
種子數據:
db/seeds.rb,
users = User.create([
???????????????????? {
?????????????????????? email: 'test-user-00@mail.com',
?????????????????????? name: 'test-user-00',
?????????????????????? activated: DateTime.now,
?????????????????????? admin: false
???????????????????? },
???????????????????? {
?????????????????????? email: 'test-user-01@mail.com',
?????????????????????? name: 'test-user-01',
?????????????????????? activated: DateTime.now,
?????????????????????? admin: false
???????????????????? }
??????????????????? ])
創建種子數據:
$ bundle exe rake db:seed
現在我們可以測試一下 api 是否正常工作, 我們可以先查看下相關 api 的路由,
$ bundle exe rake routes
輸出:
????? Prefix Verb?? URI Pattern????????????????????? Controller#Action
api_v1_users GET??? /api/v1/users(.:format)????????? api/v1/users#index
???????????? POST?? /api/v1/users(.:format)????????? api/v1/users#create
?api_v1_user GET??? /api/v1/users/:id(.:format)????? api/v1/users#show
???????????? PATCH? /api/v1/users/:id(.:format)????? api/v1/users#update
???????????? PUT??? /api/v1/users/:id(.:format)????? api/v1/users#update
???????????? DELETE /api/v1/users/:id(.:format)????? api/v1/users#destroy
啟動 rails 服務,
$ bundle exe rails s
使用 curl 請求 api,
$ curl -i http://localhost:3000/api/v1/users/1.json
{"user":{"id":1,"email":"test-user-00@mail.com","name":"test-user-00","activated":"2015-05-02T07:47:14.697Z","admin":false,"created_at":"2015-05-02T07:47:14.708Z","updated_at":"2015-05-02T07:47:14.708Z"}}
恭喜,我們的 api 工作正常!
增加認證(Authentication)
認證的過程是這樣的: 用戶把她的用戶名和密碼通過 HTTP POST 請求發送到我們的 API (在這里我們使用 sessions 端點來處理這個請求), 如果用戶名和密碼匹配,我們 會把 token 發送給用戶。 這個 token 就是用來證明用戶身份的憑證。然后在以后的每個請求中,我們都通過這個 token 來查找用戶,如果沒有找到用戶則返回 401 錯誤。
給 User 模型增加 authentication_token 屬性
$ bundle exe rails g migration add_authentication_token_to_users
db/migrate/20150502123451_add_authentication_token_to_users.rb
class AddAuthenticationTokenToUsers < ActiveRecord::Migration
? def change
??? add_column :users, :authentication_token, :string
? end
end
$ bundle exe rake db:migrate
生成 authentication_token
app/models/user.rb,
class User < ActiveRecord::Base
?+ before_create :generate_authentication_token
?+ def generate_authentication_token
?+?? loop do
?+???? self.authentication_token = SecureRandom.base64(64)
?+???? break if !User.find_by(authentication_token: authentication_token)
?+?? end
?+ end
?+ def reset_auth_token!
?+?? generate_authentication_token
?+?? save
?+ end
end
和原文相比,我給 User 模型增加了一個 reset_auth_token! 方法,我這樣做的理由主要有以下幾點:
??? 我覺得需要有一個方法幫助用戶重置 authentication token, 而不僅僅是在創建用戶時生成 authenticeation token;
??? 如果用戶的 token 被泄漏了,我們可以通過 reset_auth_token! 方法方便地重置用戶 token;
sessions endpoint
生成 sessions 控制器,
# 我們不需要生成資源文件
$ bundle exe rails g controller api/v1/sessions --no-assets
? create? app/controllers/api/v1/sessions_controller.rb
? invoke? erb
? create??? app/views/api/v1/sessions
? invoke? test_unit
? create??? test/controllers/api/v1/sessions_controller_test.rb
? invoke? helper
? create??? app/helpers/api/v1/sessions_helper.rb
? invoke??? test_unit
app/controllers/api/v1/sessions_controller.rb,
class Api::V1::SessionsController < Api::V1::BaseController
? def create
??? @user = User.find_by(email: create_params[:email])
??? if @user && @user.authenticate(create_params[:password])
????? self.current_user = @user
????? # 我們使用 jbuilder
????? # render(
????? #?? json: Api::V1::SessionSerializer.new(user, root: false).to_json,
????? #?? status: 201
????? # )
??? else
????? return api_error(status: 401)
??? end
? end
? private
? def create_params
??? params.require(:user).permit(:email, :password)
? end
end
現在我們還需要做一些原文沒有提到的工作:
??? 給 User 模型增加和 password 相關的屬性;
??? 給數據庫中已存在的測試用戶增加密碼和 authentication token;
??? 實現和 current_user 相關的方法;
??? 實現 app/views/api/v1/sessions/create.json.jbuilder;
??? 配置和 sessions 相關的路由;
給 User 模型增加和 password 相關的屬性
在 Gemfile 里將 gem 'bcrypt' 這一行的注釋取消
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
app/models/user.rb,
class User < ActiveRecord::Base
? + has_secure_password
end
給 User 模型增加 password_digest 屬性,
$ bundle exe rails g migration add_password_digest_to_users
db/migrate/20150502134614_add_password_digest_to_users.rb,
class AddPasswordDigestToUsers < ActiveRecord::Migration
? def change
??? add_column :users, :password_digest, :string
? end
end
$ bundle install
$ bundle exe rake db:migrate
給數據庫中已存在的測試用戶增加密碼和 authentication token
這個任務可以在 rails console 下完成,
首先啟動 rails console,
$ bundle exe rails c
然后在 rails console 里執行,
User.all.each {|user|
? user.password = '123123'
? user.reset_auth_token!
}
實現和 current_user 相關的方法
app/controllers/api/v1/base_controller.rb,
class Api::V1::BaseController < ApplicationController
+ attr_accessor :current_user
end
實現 app/views/api/v1/sessions/create.json.jbuilder
app/views/api/v1/sessions/create.json.jbuilder,
json.session do
? json.(@user, :id, :name, :admin)
? json.token @user.authentication_token
end
配置和 sessions 相關的路由
Rails.application.routes.draw do
? namespace :api do
??? namespace :v1 do
???? + resources :sessions, only: [:create]
??? end
? end
end
現在我們做一個測試看是否能夠順利地拿到用戶的 token, 我們使用下面的用戶作為測試用戶:
{
? email: 'test-user-00@mail.com',
? name: 'test-user-00'
}
$ curl -i -X POST -d "user[email]=test-user-00@mail.com&user[password]=123123" http://localhost:3000/api/v1/sessions.json
{"session":{"id":1,"name":"test-user-00","admin":false,"token":"izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w=="}}
我們順利地拿到了 token。
我們再做一個驗證失敗的測試。
我們使用一個錯誤的密碼: fakepwd
curl -i -X POST -d "user[email]=test-user-00@mail.com&user[password]=fakepwd" http://localhost:3000/api/v1/sessions.json
糟糕系統出錯了:
NoMethodError (undefined method `api_error' for #<Api::V1::SessionsController:0x007fead422c178>):
? app/controllers/api/v1/sessions_controller.rb:14:in `create'
原來我們沒有實現 api_error 這個方法,那我們現在就實現 api_error 這個方法。
app/controllers/api/v1/base_controller.rb,
class Api::V1::BaseController < ApplicationController
?+ def api_error(opts = {})
?+?? render nothing: true, status: opts[:status]
?+ end
end
繼續測試:
curl -i -X POST -d "user[email]=test-user-00@mail.com&user[password]=fakepwd" http://localhost:3000/api/v1/sessions
HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/plain; charset=utf-8
Cache-Control: no-cache
X-Request-Id: a5349b47-d756-4830-84f8-0653577f936d
X-Runtime: 0.319768
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Sat, 02 May 2015 14:41:55 GMT
Content-Length: 0
Connection: Keep-Alive
此時服務器返回了 401 Unauthorized
Authenticate User
在前面的測試中,我們已經成功地拿到了用戶的 token, 那么現在我們把 token 和 email 發給 API
看能否成功識別出用戶。
首先在 Api::V1::BaseController 里實現 authenticate_user! 方法:
app/controllers/api/v1/base_controller.rb,
class Api::V1::BaseController < ApplicationController
+? def authenticate_user!
+??? token, options = ActionController::HttpAuthentication::Token.token_and_options(request)
+??? user_email = options.blank?? nil : options[:email]
+??? user = user_email && User.find_by(email: user_email)
+??? if user && ActiveSupport::SecurityUtils.secure_compare(user.authentication_token, token)
+????? self.current_user = user
+??? else
+????? return unauthenticated!
+??? end
+? end
end
ActionController::HttpAuthentication::Token 是 rails 自帶的方法,可以參考 rails 文檔 了解其詳情。
當我們通過 user_email 拿到 user 后, 通過 ActiveSupport::SecurityUtils.secure_compare
對 user.authentication_token 和從請求頭里取到的 token 進行比較,如果匹配則認證成功,否則返回
unauthenticated!。這里使用了 secure_compare 對字符串進行比較,是為了防止時序攻擊(timing attack)
我們構造一個測試用例, 這個測試用例包括以下一些步驟:
??? 用戶登錄成功, 服務端返回其 email, token 等數據
??? 用戶請求 API 更新其 name, 用戶發送的 token 合法, 更新成功
??? 用戶請求 API 更新其 name, 用戶發送的 token 非法, 更新失敗
為了讓用戶能夠更新其 name, 我們需要實現 user update API, 并且加入 before_action :authenticate_user!, only: [:update]
app/controllers/api/v1/users_controller.rb,
class Api::V1::UsersController < Api::V1::BaseController
+ before_action :authenticate_user!, only: [:update]
+ def update
+?? @user = User.find(params[:id])
+?? @user.update_attributes(update_params)
+ end
+ private
+ def update_params
+?? params.require(:user).permit(:name)
+ end
end
app/views/api/v1/users/update.json.jbuilder,
json.user do
? json.(@user, :id, :name)
end
現在我們進行測試, 測試用戶是:
{
? id: 1,
? email: 'test-user-00@mail.com',
? name: 'test-user-00',
? authentication_token: 'izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w=='
}
$ curl -i -X PUT -d "user[name]=gg-user" \
? --header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==, \
? email=test-user-00@mail.com" \
? http://localhost:3000//api/v1/users/1
{"user":{"id":1,"name":"gg-user"}} ?
我們看到 user name 已經成功更新為 gg-user。
讀者們請注意: 你們自己測試時需要將 token 換為你們自己生成的 token。
我們使用一個非法的 token 去請求 API, 看看會發生什么狀況。
curl -i -X PUT -d "user[name]=bb-user" \
? --header "Authorization: Token token=invalid token, \
? email=test-user-00@mail.com" \
? http://localhost:3000//api/v1/users/1
服務器出現錯誤:
NoMethodError (undefined method `unauthenticated!' for #<Api::V1::UsersController:0x007fead6108d80>)
接下來我們實現 unauthenticated! 方法。
app/controllers/api/v1/base_controller.rb,
class Api::V1::BaseController < ApplicationController
+ def unauthenticated!
+?? api_error(status: 401)
+ end
end
繼續上面的測試:
curl -i -X PUT -d "user[name]=bb-user" \
? --header "Authorization: Token token=invalid token, \
? email=test-user-00@mail.com" \
? http://localhost:3000//api/v1/users/1
HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/plain; charset=utf-8
Cache-Control: no-cache
X-Request-Id: 8cf07968-1fd0-4041-866a-ddea49af11d3
X-Runtime: 0.005578
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Sun, 03 May 2015 05:51:52 GMT
Content-Length: 0
Connection: Keep-Alive ?
服務器返回 401 Unauthorized, 并且 user name 沒有被更新。
增加授權(Authorization)
上面的測試有個問題,就是當前登錄的用戶可以把其他用戶的 name 更新,這個應該是不被允許的,所以我們 還需要增加一個權限認證的機制。在這里我們使用 Pundit 來 實現權限認證。
安裝 pundit
Gemfile,
+ gem 'pundit'
$ bundle install
app/controllers/api/v1/base_controller.rb,
class Api::V1::BaseController < ApplicationController
? + include Pundit
end
$ bundle exe rails g pundit:install
create? app/policies/application_policy.rb
將 policies 目錄放到 rails 的自動加載路徑中:
config/application.rb,
module BuildAnApiRailsDemo
? class Application < Rails::Application
+??? config.autoload_paths << Rails.root.join('app/policies')
? end
end
創建和 user 相關的權限機制
app/policies/user_policy.rb,
class UserPolicy < ApplicationPolicy
? def show?
??? return true
? end
? def create?
??? return true
? end
? def update?
??? return true if user.admin?
??? return true if record.id == user.id
? end
? def destroy?
??? return true if user.admin?
??? return true if record.id == user.id
? end
? class Scope < ApplicationPolicy::Scope
??? def resolve
????? scope.all
??? end
? end
end
使用 UserPolicy
app/controllers/api/v1/users_controller.rb,
class Api::V1::UsersController < Api::V1::BaseController
? def update
??? @user = User.find(params[:id])
+?? return api_error(status: 403) if !UserPolicy.new(current_user, @user).update?
??? @user.update_attributes(update_params)
? end
end
測試:
$ curl -i -X PUT -d "user[name]=gg-user" \
? --header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==, \
? email=test-user-00@mail.com" \
? http://localhost:3000//api/v1/users/2.json
HTTP/1.1 403 Forbidden
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
注意我們測試的 url 地址是 http://localhost:3000//api/v1/users/2, 也就是說我們在更新 id 為 2 的那個用戶的 name。此時服務器返回的是 403 Forbidden。
pundit 提供了更簡便的 authorize 方法為我們做權限認證的工作。
class Api::V1::UsersController < Api::V1::BaseController
? def update
??? @user = User.find(params[:id])
??? # return api_error(status: 403) if !UserPolicy.new(current_user, @user).update?
+?? authorize @user, :update?
??? @user.update_attributes(update_params)
? end
end
測試:
$ curl -i -X PUT -d "user[name]=gg-user" \
? --header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==, \
? email=test-user-00@mail.com" \
? http://localhost:3000//api/v1/users/2.json
此時服務器報 Pundit::NotAuthorizedError 錯誤,
Pundit::NotAuthorizedError (not allowed to update?
我們可以使用 rescue_from 捕捉 Pundit::NotAuthorizedError 這類異常。
class Api::V1::BaseController < ApplicationController
? include Pundit
+? rescue_from Pundit::NotAuthorizedError, with: :deny_access
+? def deny_access
+??? api_error(status: 403)
+? end
end
測試:
$ curl -i -X PUT -d "user[name]=gg-user" \
? --header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==, \
? email=test-user-00@mail.com" \
? http://localhost:3000//api/v1/users/2
HTTP/1.1 403 Forbidden
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/plain; charset=utf-8
這次服務器直接返回 403 Forbidden
分頁
我們現在要實現一個展示用戶發的微博的 API, 如果用戶的微博數量很多,那么我們應該用上分頁。
建立 Micropost 模型
$ bundle exe rails g model Micropost
db/migrate/20150503131743_create_microposts.rb,
class CreateMicroposts < ActiveRecord::Migration
? def change
??? create_table :microposts do |t|
????? t.string :title
????? t.text :content
????? t.integer :user_id
????? t.timestamps null: false
??? end
? end
end
執行:
$ bundle exe rake db:migrate
為 id 為 1 的用戶創建 100 條微博紀錄:
lib/tasks/data.rake,
namespace :data do
? task :create_microposts => [:environment] do
??? user = User.find(1)
??? 100.times do |i|
????? Micropost.create(user_id: user.id, title: "title-#{i}", content: "content-#{i}")
??? end
? end
end
執行:
$ bundle exe rake data:create_microposts
Api::V1::MicropostsController
執行:
$ bundle exe rails g controller api/v1/microposts --no-assets
配置路由:
config/routes.rb,
Rails.application.routes.draw do
? namespace :api do
??? namespace :v1 do
?+??? scope path: '/user/:user_id' do
?+????? resources :microposts, only: [:index]
?+??? end
??? end
? end
end
此時和 microposts 相關的路由如下:
api_v1_microposts GET??? /api/v1/user/:user_id/microposts(.:format) api/v1/microposts#index
我們使用 kaminari 這個 gem 進行分頁。
安裝 kaminari,
Gemfile
+ gem 'kaminari'
執行:
$ bundle install
app/models/user.rb
class User < ActiveRecord::Base
?+ has_many :microposts
end
app/controllers/api/v1/microposts_controller.rb
class Api::V1::MicropostsController < Api::V1::BaseController
+? def index
+??? user = User.find(params[:user_id])
+??? @microposts = paginate(user.microposts)
+? end
end
app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ApplicationController
? def paginate(resource)
??? resource = resource.page(params[:page] || 1)
??? if params[:per_page]
????? resource = resource.per(params[:per_page])
??? end
??? return resource
? end
end
app/helpers/application_helper.rb
module ApplicationHelper
+? def paginate_meta_attributes(json, object)
+??? json.(object,
+????????? :current_page,
+????????? :next_page,
+????????? :prev_page,
+????????? :total_pages,
+????????? :total_count)
+? end
end
app/views/api/v1/microposts/index.json.jbuilder,
json.paginate_meta do
? paginate_meta_attributes(json, @microposts)
end
json.microposts do
? json.array! @microposts do |micropost|
??? json.(micropost, :id, :title, :content)
? end
end
測試:
$ curl -i -X GET http://localhost:3000/api/v1/user/1/microposts.json?per_page=3
{
??? "paginate_meta": {
??? "current_page":1,
??? "next_page":2,
??? "prev_page":null,
??? "total_pages":34,
??? "total_count":100
??? },
??? "microposts":[
??? {"id":1,"title":"title-0","content":"content-0"},
??? {"id":2,"title":"title-1","content":"content-1"},
??? {"id":3,"title":"title-2","content":"content-2"}
??? ]
}
API 調用頻率限制(Rate Limit)
我們使用 redis-throttle 來實現這個功能。
Gemfile,
gem 'redis-throttle', git: 'git://github.com/andreareginato/redis-throttle.git'
執行,
$ bundle install
集成到 Rails 中:
config/application.rb,
# At the top of config/application.rb
+ require 'rack/redis_throttle'
class Application < Rails::Application
? # Limit the daily number of requests to 3
? # 為了測試我們把 limit 設置為 3
+ config.middleware.use Rack::RedisThrottle::Daily, max: 3
end
我們開始測試,請求 http://localhost:3000/api/v1/users/1 4次看會出現什么結果。
前面 3 次請求一切正常,
curl -i http://localhost:3000/api/v1/users/1
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
X-Ratelimit-Limit: 3
X-Ratelimit-Remaining: 0
Etag: W/"eb58510a43ebc583cf61de35b6d20093"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: bbe7437b-ba6e-4cfd-a4ef-49eec4c611fd
X-Runtime: 0.014384
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Thu, 07 May 2015 13:03:31 GMT
Content-Length: 199
Connection: Keep-Alive
{"user":{"id":1,"email":"test-user-00@mail.com","name":"gg-user","activated":"2015-05-02T07:47:14.697Z","admin":false,"created_at":"2015-05-02T07:47:14.708Z","updated_at":"2015-05-03T05:40:24.931Z"}}
我們注意服務器返回的兩個響應頭: X-Ratelimit-Limit 和 X-Ratelimit-Remaining,
X-Ratelimit-Limit 的值一直為3,表示請求的限制值,
而 X-Ratelimit-Remaining 每請求一次,其值會減 1, 直到為 0。
第 4 次請求出現 403 Forbidden, 這說明 redis-throttle 起到了其應有的作用。
curl -i http://localhost:3000/api/v1/users/1
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
Cache-Control: no-cache
X-Request-Id: fd646f00-a6a8-411d-b5e4-24856c63b078
X-Runtime: 0.002375
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Thu, 07 May 2015 13:03:33 GMT
Content-Length: 35
Connection: Keep-Alive
403 Forbidden (Rate Limit Exceeded)
redis-throttle 的 redis 連接默認是 redis://localhost:6379/0, 你也可以通過設置環境變量
ENV['REDIS_RATE_LIMIT_URL'] 來改變 redis-throttle 的 redis 連接。
CORS
CORS 是 Cross Origin Resource Sharing 的縮寫。簡單地說 CORS 可以允許其他域名的網頁通過 AJAX 請求你的 API。
我們可以使用 rack-cors gem 來幫助我們的 API 實現 CORS。
Gemfile,
+ gem 'rack-cors'
config/application.rb,
module BuildAnApiRailsDemo
? class Application < Rails::Application
+??? config.middleware.insert_before 0, "Rack::Cors" do
+????? allow do
+??????? origins '*'
+??????? resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options, :head]
+????? end
+??? end
? end
end
Version 2 API
隨著我們的業務發展,我們的 API 需要做較大的改變,同時我們需要保持 Version 1 API, 所以我們 開始開發 Version 2 API。
routes
config/routes.rb,
Rails.application.routes.draw do
? namespace :api do
??? namespace :v1 do
????? resources :users, only: [:index, :create, :show, :update, :destroy]
????? # resources :microposts, only: [:index, :create, :show, :update, :destroy]
????? resources :sessions, only: [:create]
????? scope path: '/user/:user_id' do
??????? resources :microposts, only: [:index]
????? end
??? end
+??? namespace :v2 do
+????? resources :users, only: [:index, :create, :show, :update, :destroy]
+????? resources :sessions, only: [:create]
+????? scope path: '/user/:user_id' do
+??????? resources :microposts, only: [:index]
+????? end
+??? end
? end
end
controller
生成 API::V2::UsersController, 其他控制器的生成類似
$ bundle exe rails g controller api/v2/users --no-assets
app/controllers/api/v2/users_controller.rb,
class Api::V2::UsersController < Api::V1::UsersController
? def show
??? @user = User.find(params[:id])
? end
end
app/vies/api/v2/users/show.json.jbuilder,
json.user do
? json.(@user, :id, :email, :name,? :activated, :admin, :created_at, :updated_at)
end
測試:
$ curl -i http://localhost:3000/api/v2/users/1.json
{"user":{"id":1,"email":"test-user-00@mail.com","name":"gg-user","activated":"2015-05-02T07:47:14.697Z","admin":false,"created_at":"2015-05-02T07:47:14.708Z","updated_at":"2015-05-03T05:40:24.931Z"}}%?? ?
文檔
原文提到了下面的幾種文檔工具:
??? swagger-rails 和 swagger-docs
??? apipie-rails
??? slate
和原文一樣,我也喜歡使用 slate 作為文檔工具.
將 slate 集成到項目中
創建 docs 目錄,
$ mkdir app/docs
集成 slate,
$ cd app/docs
$ git clone git@github.com:tripit/slate.git
$ rm -rf slate/.git
$ cd slate
$ bundle install
配置構建目錄, app/docs/slate/config.rb
+ set :build_dir, '../../../public/docs/'
現在我們開始編寫獲取用戶信息這個 API 的文檔。
app/docs/slate/source/index.md,
---
title: API Reference
language_tabs:
? - ruby
toc_footers:
? - <a href='http://github.com/tripit/slate'>Documentation Powered by Slate</a>
includes:
? - errors
search: true
---
# 介紹
API 文檔
# 獲取用戶信息
## V1
## HTTP 請求
`GET http://my-site/api/v1/users/<id>`
## 請求參數
參數名 | 是否必需 | 描述
-----| --------| -------
id?? |? 是????? | 用戶 id|
## 響應
\```json
{
? "user":
? {
??? "id":1,
??? "email":"test-user-00@mail.com",
??? "name":"test-user-00",
??? "activated":"2015-05-02T07:47:14.697Z",
??? "admin":false,
??? "created_at":"2015-05-02T07:47:14.708Z",
??? "updated_at":"2015-05-02T07:47:14.708Z"
?? }
}
\```
注意: index.md 范例里的json代碼語法高亮部分有轉義字符, 直接復制可能沒法看到語法高亮效果, 在實際使用時需要將 ``` 前面的 '\' 符號去掉。
build 腳本
docs_build.sh,
#!/bin/bash
cd app/docs/slate
bundle exec middleman build --clean
build docs,
$ chmod +x docs_build.sh
$ ./docs_build.sh
可以通過 http://localhost:3000/docs/index.html 訪問文檔
給 API 文檔添加訪問控制
配置路由:
routes.rb,
+ get '/docs/index', to: 'docs#index'
建立相關控制器:
$ bundle exe rails g controller docs
app/controllers/docs_controller.rb,
class DocsController < ApplicationController
? USER_NAME, PASSWORD = 'doc_reader', '123123'
? before_filter :basic_authenticate
? layout false
? def index
? end
? private
? def basic_authenticate
??? authenticate_or_request_with_http_basic do |user_name, password|
????? user_name == USER_NAME && password == PASSWORD
??? end
? end
end
同時我們需要把 public/docs/index.html 文件轉移到 app/views/docs/ 目錄下面, 我們
可以更改 docs_build.sh 腳本, 注意 docs_build.sh 應該放在項目的根目錄下, 比如: /path/to/build-an-api-rails-demo/docs_build.sh,
#!/bin/bash
app_dir=`pwd`
cd $app_dir/app/docs/slate
bundle exec middleman build --clean
cd $app_dir
mv $app_dir/public/docs/index.html $app_dir/app/views/docs
重新 build 文檔,
$ ./docs_build.sh
瀏覽器訪問 http://localhost:3000/docs/index.html,
提示需要輸入用戶名和密碼,我們輸入正確的用戶名(doc_reader)和密碼(123123)后就可以正常訪問文檔了,
轉載于:https://www.cnblogs.com/lv-books/p/6375065.html
總結
以上是生活随笔為你收集整理的rails 构建 API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: hbase shell-dml(数据管理
- 下一篇: [LeetCode] Max Point