根据使用 Rails 构建 API 实践帖子改写,使用 Active Model Serializers 输出 因为是纯新手,所以想先盯着输出这里的东西,所以其他的东西,我就抱歉啦,标题代码逻辑什么的我都直接照抄啦,请不要告我侵权ಠ౪ಠ 全部完成后会 at 原作者的 @kayakjiang
完成原文前 5 章,后续不打算做了,一个是因为后面只是部分 gem 的使用,还有版本控制
还没找到合适的解决方案。
单独发过一个帖子,在使用 AMS 做 JSONAPI 的过程中,我不知是不是因为自己不会使用的关系,导致返回的媒体类型为 application/json,而不是 JSONAPI 所注册过的 vpn.api+json.所以暂时通过以下方法进行更改:
api_mime_types = %W(
application/vnd.api_json
text/x-json
applicaiton/json
)
Mime::Type.register 'applicaiton/vnd.api+json', :json, api_mime_types
即可将返回的媒体类型修改为正确的 vpn.api+json,但是总感觉这种方法不是太合适。
rails new demo --api
所需 Gem 会逐渐列出
resources :users, only: [:index, :create, :show, :update, :destroy]
生成控制器:rails g controller usesrs
class UsersController < BaseController
def show
@user = User.find(params[:id])
render json: @user
end
def index
render json: User.all
end
end
安装 gem:gem 'active_model_serializers'
bundle install
:attributes
(default):json
:json_api
也可在具体文件内单独指定 Adapter 类型 (如:
adapter: :json_api
)
ActiveModel::Serializer.config.adapter = :json_api
生成 Serializer:rails g serializer user
class Api::V1::UserSerializer < ActiveModel::Serializer
attributes :id,:name,:email,:activated,:admin
end
rails g model User
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
数据迁移:rails db:migrate
种子数据:
users = User.create([
{
email: '[email protected]',
name: 'test00',
activated: DateTime.now,
admin: false
},
{
email: '[email protected]',
name: 'test01',
activated: DateTime.now,
admin: false
},
{
email: '[email protected]',
name: 'test02',
activated: DateTime.now,
admin: false
},
{
email: '[email protected]',
name: 'test03',
activated: DateTime.now,
admin: false
},
{
email: '[email protected]',
name: 'test04',
activated: DateTime.now,
admin: false
}])
创建种子数据:rails db:seed
启动 rails 服务器:rails s
使用 curl 请求:curl -i http://localhost:3000/users/1.json
得到结果 (3 种 Adapter 为显示区别均已列出,后续部分只输出 JSON_API)
#JSON_API
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
ETag: W/"26bfbe5f8e1fb23b6069c3625394a924"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 75723ba3-d2e6-4099-9ffa-be3daccf673b
X-Runtime: 0.002995
Transfer-Encoding: chunked
{"data":{"id":"1","type":"users","attributes":{"name":"test00","email":"[email protected]","activated":"2016-07-20T05:20:11.904Z","admin":false}}}
#JSON
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
ETag: W/"5b726a6e35cc53e1e21dab387723cf2d"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: ac41ba6b-269d-4d9e-9d10-1b276fcced47
X-Runtime: 0.126139
Transfer-Encoding: chunked
{"user":{"id":1,"name":"test00","email":"[email protected]","activated":"2016-07-20T05:20:11.904Z","admin":false}}
#attributes
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
ETag: W/"f90830c5ced0658dc653b158c52ffb3d"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 3fc863d4-2b9f-497b-94dc-adb840af82e3
X-Runtime: 0.124103
Transfer-Encoding: chunked
{"id":1,"name":"test00","email":"[email protected]","activated":"2016-07-20T05:20:11.904Z","admin":false}
rails g migration add_authentication_token_to_users
class AddAuthenticationTokenToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :authentication_token, :string
end
end
rails db:migrate
class User < ApplicationRecord
before_create :generate_authentication_token
has_secure_password
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
生成 sessions 控制器:rails g controller sessions
class SessionsController < ApplicationController
def create
@user = User.find_by(email: create_params[:email])
if @user && @user.authenticate(create_params[:password])
self.current_user = @user
render json: current_user, serializer: SessionSerializer
#必须显示指定serializer,不知原因,求解释
#应该是根据对象类型自动匹配到user上了
#也可以直接修改UserSerializer添加token字段
else
return api_error(status: 401)
end
end
private
def create_params
params.require(:user).permit(:email,:password)
end
end
安装 Gem:gem 'bcrypt'
bundle install
class User < ApplicationRecord
+ has_secure_password
end
增加 password_digest 属性:rails g migration add_password_digest_to_users
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :password_digest, :string
end
end
rails db:migrate
rails c
打开终端,执行:
User.all.each {|user|
user.password = '123123'
user.reset_auth_token!
}
class ApplicationController < ActionController::API
+ attr_accessor :current_user
end
lass ApplicationController < ActionController::API
#原文使用render nothing: true方法
#运行时报warning说5.1要移除nothing了
#这里改为官方建议的head方法
+ def api_error(opts = {})
+ render head: :unauthorized, status: opts[:status]
+ end
end
生成 Serializer:rails g serializer session
class SessionSerializer < ActiveModel::Serializer
attributes :id,:name,:admin,:token
def token
object.authentication_token
end
end
如上文所说,可以不要这个 Serializer,直接修改 UserSerializer 就可以
Rails.application.routes.draw do
+ resources :sessions, only: [:create]
end
url -i -X POST -d "user[email][email protected]&user[password]=123123" http://localhost:3000/sessions
得到结果:
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
ETag: W/"b71c6480196ab554d31d298a293adc75"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0e0507cf-81a1-4972-aaa0-764945bd820c
X-Runtime: 0.093730
Transfer-Encoding: chunked
{"data":{"id":"1","type":"users","attributes":{"name":"test00","admin":false,"token":"zKif6laUSlSJY4EDi3owNcJEFPKm87bvFKpXMLTROMRT2DZrbtsBMBWJalab6wS96a3ntR7yTar5Z3yVYEDeOg=="}}}
顺利得到了 token
curl -i -X POST -d "user[email][email protected]&user[password]=123" http://localhost:3000/sessions
密码改为错误的"123"
得到结果:
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: 99856444-b644-43d4-99db-34c1a2afac4d
X-Runtime: 0.068964
Transfer-Encoding: chunked
我们得到了 401 Unauthorized 错误。
首先实现 authenticate_user! 方法
class ApplicationController < ActionController::API
+ 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
然后实现 update API,并加入before_action :authenticate_user!, only:[:update]
lass UsersController < ApplicationController
before_action :authenticate_user!, only: [:update]
+ def update
+ @user = User.find(params[:id])
+ @user.update_attributes(update_params)
+ render json: @user
+ end
private
def update_params
params.require(:user).permit(:name)
end
end
再加入认证失败返回的方法
class ApplicationController < ActionController::API
+ def unauthenticated!
+ api_error(status: 401)
+ end
end
使用 curl 提交修改 2 号用户的用户名:
curl -i -X PUT -d "user[name]=gg-user" --header "Authorization: Token token=4Vrayf/5g+720JRWQV4BQkk+uT5UeBmgR6LsacBNJfCwXWI3rgNknN97pyxsg8YWJIm0oNq4vKdqIhcatcHhsQ==, [email protected]" http://localhost:3000/users/2
得到结果:
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
ETag: W/"d6a64e0caf907e3c60b69c31c2a9d23d"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 054a8ccc-ac68-4cd6-8fb1-6ac33ee191f4
X-Runtime: 0.611735
Transfer-Encoding: chunked
{"data":{"id":"2","type":"users","attributes":{"name":"gg-user","email":"[email protected]","activated":"2016-07-20T05:20:11.905Z","admin":false,"authentication-token":"4Vrayf/5g+720JRWQV4BQkk+uT5UeBmgR6LsacBNJfCwXWI3rgNknN97pyxsg8YWJIm0oNq4vKdqIhcatcHhsQ=="}}}
可以看到用户名已经被更改了;再使用错误的 token 看看效果:
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: ac4241f9-7125-4142-a281-3ba060061b9f
X-Runtime: 0.002108
Transfer-Encoding: chunked
返回 401 错误,如愿。
安装 Gem: gem 'pundit'
bundle install
class ApplicationController < ActionController::API
+ include Pundit
end
执行安装:rails g pundit:install
会生成一个app/policies/application_policy.rb
文件
class ApplicationController < ActionController::API
+ include Pundit
end
将 policies 目录放入 rails 自动加载路径中
module ApiDemo
class Application < Rails::Application
+ config.autoload_paths << Rails.root.join('app/policies')
end
end
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
class UsersController < ApplicationController
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)
render json: @user
end
end
捕获抛出的异常:
class ApplicationController < ActionController::API
+ rescue_from Pundit::NotAuthorizedError, with: :deny_access
+ def deny_access
+ api_error(status: 403)
+ end
end
使用 user/1 的数据去更新 user/2 的:
curl -i -X PUT -d "user[name]=gg22-user" --header "Authorization: Token token=zKif6laUSlSJY4EDi3owNcJEFPKm87bvFKpXMLTROMRT2DZrbtsBMBWJalab6wS96a3ntR7yTar5Z3yVYEDeOg==, [email protected]" http://localhost:3000/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
Cache-Control: no-cache
X-Request-Id: 3263a340-13af-44ab-ad04-872a015f79fe
X-Runtime: 0.018907
Transfer-Encoding: chunked
返回 403,如愿。
rails g model Micropost
class CreateMicroposts < ActiveRecord::Migration[5.0]
def change
create_table :microposts do |t|
t.string :title
t.text :content
t.integer :user_id
t.timestamps null:false
end
end
end
执行迁移:rails db:migrate
+ 100.times do |i|
+ Micropost.create(user_id: 1, title: "title-#{i}", content: "content-#{i}")
+ end
rails db:seed
生成控制器:rails g controller microposts
配置路由:
Rails.application.routes.draw do
+ scope path: '/user/:user_id' do
+ resources :microposts, only[:index]
+ end
end
class MicropostsController < ApplicationController
def index
user = User.find(params[:user_id])
#使用分页函数
@microposts = paginate(user.microposts)
#添加meta数据方法
render json: @microposts, meta: paginate_meta(@microposts)
end
end
使用 kaminair 进行分页
安装 Gem:gem 'kaminari'
bundle install
class ApplicationController < ActionController::API
+ def paginate_meta(resource)
+ {
+ current_page: resource.current_page,
+ next_page: resource.next_page,
prev_page: resource.prev_page,
+ total_pages: resource.total_pages,
+ total_count: resource.total_count
+ }
+ end
#对于新手(我)来说,关于参数的深坑...
#构造url的时候,参数之间请添加转义符连接,不要单独使用&
#不要问我怎么知道的...😂
+ def paginate(resource)
+ resource = resource.page(params[:page] || 1)
+ if params[:per_page]
+ resource = +resource.per(params[:per_page])
+ end
+ return resource
+ end
end
生成 Serializer:rails g serializer micropost
class MicropostSerializer < ActiveModel::Serializer
attributes :id,:title,:content
end
class User < ApplicationRecord
+ has_many :microposts
end
class Micropost < ApplicationRecord
belongs_to :users
end
curl -i -X GET http://localhost:3000/user/1/microposts?per_page=3
得到结果:
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
ETag: W/"c1d821a3d3a878d1d86b3819c8589f39"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 4f4a6d88-8041-4ac3-ad10-02a3ed4bf886
X-Runtime: 0.033613
Transfer-Encoding: chunked
{
"data": [
{
"id": "1",
"type": "microposts",
"attributes": {
"title": "title-0",
"content": "content-0"
}
},
{
"id": "2",
"type": "microposts",
"attributes": {
"title": "title-1",
"content": "content-1"
}
},
{
"id": "3",
"type": "microposts",
"attributes": {
"title": "title-2",
"content": "content-2"
}
}
],
"links": {
"self": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=1&page%5Bsize%5D=3&per_page=3",
"next": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=2&page%5Bsize%5D=3&per_page=3",
"last": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=34&page%5Bsize%5D=3&per_page=3"
},
"meta": {
"current-page": 1,
"next-page": 2,
"prev-page": null,
"total-pages": 34,
"total-count": 100
}
}
curl -i -X GET http://localhost:3000/user/1/microposts?per_page=3\&page=8
得到结果:
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
ETag: W/"27662403e9ec3ef2fca76266b97cbdb6"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 1f17e152-b4a7-4176-aec9-915a689bab10
X-Runtime: 0.006062
Transfer-Encoding: chunked
{
"data": [
{
"id": "22",
"type": "microposts",
"attributes": {
"title": "title-21",
"content": "content-21"
}
},
{
"id": "23",
"type": "microposts",
"attributes": {
"title": "title-22",
"content": "content-22"
}
},
{
"id": "24",
"type": "microposts",
"attributes": {
"title": "title-23",
"content": "content-23"
}
}
],
"links": {
"self": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=8&page%5Bsize%5D=3&per_page=3",
"first": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=1&page%5Bsize%5D=3&per_page=3",
"prev": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=7&page%5Bsize%5D=3&per_page=3",
"next": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=9&page%5Bsize%5D=3&per_page=3",
"last": "http://localhost:3000/user/1/microposts?page%5Bnumber%5D=34&page%5Bsize%5D=3&per_page=3"
},
"meta": {
"current-page": 8,
"next-page": 9,
"prev-page": 7,
"total-pages": 34,
"total-count": 100
}
}
如果使用 JSON_API,会自动添加 links
curl 里的连接,如果需要传递多个参数,参数之间请使用\&
,不要单独使用&