Rails 使用 Rails 构建 API 实践

kayakjiang · 2015年05月31日 · 最后由 judi0713 回复于 2018年03月20日 · 59179 次阅读
本帖已被管理员设置为精华贴

我是来鼓吹使用 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: '[email protected]',
                       name: 'test-user-00',
                       activated: DateTime.now,
                       admin: false
                     },
                     {
                       email: '[email protected]',
                       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":"[email protected]","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! 方法,我这样做的理由主要有以下几点:

  1. 我觉得需要有一个方法帮助用户重置 authentication token, 而不仅仅是在创建用户时生成 authenticeation token;

  2. 如果用户的 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

现在我们还需要做一些原文没有提到的工作:

  1. 给 User 模型增加和 password 相关的属性;

  2. 给数据库中已存在的测试用户增加密码和 authentication token;

  3. 实现和 current_user 相关的方法;

  4. 实现 app/views/api/v1/sessions/create.json.jbuilder;

  5. 配置和 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: '[email protected]',
  name: 'test-user-00'
}
$ curl -i -X POST -d "user[email][email protected]&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][email protected]&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][email protected]&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)

我们构造一个测试用例,这个测试用例包括以下一些步骤:

  1. 用户登录成功,服务端返回其 email, token 等数据

  2. 用户请求 API 更新其 name, 用户发送的 token 合法,更新成功

  3. 用户请求 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: '[email protected]',
  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 protected]" \
  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 protected]" \
  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 protected]" \
  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 protected]" \
  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 protected]" \
  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 protected]" \
  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":"[email protected]","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":"[email protected]","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"}}%    

文档

原文提到了下面的几种文档工具:

  1. swagger-railsswagger-docs

  2. apipie-rails

  3. slate

和原文一样,我也喜欢使用 slate 作为文档工具。

将 slate 集成到项目中

创建 docs 目录,

$ mkdir app/docs

集成 slate,

$ cd app/docs

$ git clone [email protected]: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":"[email protected]",
    "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) 后就可以正常访问文档了,

项目代码

build-an-api-rails-demo

感谢 @lazybios 同学提的 5 点建议,让本文变的更好。

感谢 @night_7th 提的注明 skip_before_filter :verify_authenticity_token 的建议。

#1 楼 @rei 竟然有这么好的例子,感谢

文档方面我在用 RAML(http://raml.org/) 感觉写起来比 markdown 方便

:plus1: 好全,学习了

#4 楼 @suupic markdown 最大的好处是它本身的可读性对人类非常友好

思路是对的,没有必要为了写 API 而另准备一套服务器

匿名 #10 2015年06月01日

:plus1:

Good! 正是我所需要的

请问楼主 generate_authentication_token 中为什么要用 loop 呢?

哦,是要保证 token 的唯一性。。。

#13 楼 @lithium4010 避免生成重复的 authentication_token

#6 楼 @kayakjiang

RAML 专门用来描述 REST API, 可读性要比 markdown 好

之前用过 slate,感觉 markdown 文件一大了书写和修改都十分头疼

RAML 使用 YAML 语法,许多通用的属性可以嵌套引用,能省掉很多功夫 同时也支持内嵌 markdown 描述复杂的内容

也有一个 webUI,raml 文件拖进去就能跑,webUI 还支持模拟请求 https://github.com/mulesoft/api-console

供参考

最近也在弄 API,好纠结,最后还是用了 grape 来实现。

一开始也打算这么写,后来看了下感觉代码量也没有少,还往 routes 里增加了不少代码,所以有人能够解释下这样写有什么好处不?

#16 楼 @flingfox63 对啊,用 Grape 写,代码量也没有少,routes 的代码都移到 grape 里面,调用栈增加了,跟 Rails 的 Filter 和 Helper 不通用了,有人解释下 Grape 有什么好处么?

#17 楼 @rei 据爱用 Grape 的人说,他们觉得用 Grape 就能控制住一切,Rails 太复杂,他们感觉控制不了。 我觉得,他们可能需要控制了 Ruby 解释器的代码~

#15 楼 @suupic 看了下视频,RAML 的效果确实很惊艳,有机会试试

#16 楼 @flingfox63 以后有机会还是试试用 Rails 写 API,其实写 API 和平时开发 web 是一样的,没有必要再引入其他的框架去单独写 API,另外 Rails 的路由非常棒,通过路由你能够了解一个复杂项目的脉络。

#17 楼 @rei Grape 和 rails api 都写过。Grape 也许是另一种舒适的代码体验

虽然正在用 Grape,但早就习惯 rails 这一套,还是觉得 rails 用起来方便一些,效率也不会差多少。

很全,赞!

太好了,要什么来什么,正要找这样的文章就看到了,呵呵。

目前我用 Grape + Goliath 的组合。使用了 Activerecord,然后和 Rails 共享 model 层的代码。扩展了 Grape 让它支持 html 渲染,以便提供 API 文档页面,自己写了发布代码以便不要和 Rails 发布产生交叉影响;单独使用了 whenever,增加了几个类似 Rails 里面的目录来放 rake task 文件和 schedule 文件,让 Api 的后台脚本与 Rails 分开管理。这样 Rails 就可以单独做后台管理,特别是发布和测试的时候,不会影响前端 API;还可以随意增加 Rails 项目降低耦合度。

很棒,支持了

文章很赞!

@suupic RAML 貌似是个很不错的选择。

之前是用 swagger 在寫公司內部的 API 文檔,不過後來發現靜態 HTML 就足夠滿足需求,所以找了

https://github.com/kevinrenskers/raml2html https://www.npmjs.com/package/raml2html

雖然不是 gem,但相當好用,有需要的朋友可以參考下

写的很好,赞一个!

我觉得直接用 ApplicationController 还是太重了,可以继承 ActionController::Metal,然后一个个把需要的东西包含进来,性能岂不是更好?

我现在的项目是 rails+greap,当时写 API 时考虑过使用 rails-api,直接 rails,greap,3 选一,最后让我确定用 greap 的理由是,后台啥的全是写 rails,偶尔换换口味写写其他风格的代码,不是果断可以调节下么,而且 greap 的部分优势其实还是有目共睹的

33 楼 已删除

不错不错

你有做 spec controller 吗?

require 'rails_helper'
require 'awesome_print'

RSpec.describe Api::V1::UsersController, type: :controller do
  describe "GET #show" do
    before do
      @user = create :user
      @request.headers['Authorization'] = "Token token=#{@user.authentication_token}, email=#{@user.email}"  # 问题point
    end

    after do
      @request.headers['Authorization'] = nil
    end

    it "returns http success" do
      get :show, { id: @user.to_param, format: :json }
      ap response.body  # 此处有一个奇怪的反应,打印 "" 这个""不知从何处而来。原因是加入了@request.headers['Authorization'] = "xxx“的设置,去掉就正常返回。
      json_hash = JSON.parse(response.body)
      expect(json_hash['code']).to eq(200)
    end
  end
end

console log

""
F

Failures:

  1) Api::V1::UsersController GET #show returns http success
     Failure/Error: json_hash = JSON.parse(response.body)
     JSON::ParserError:
       A JSON text must at least contain two octets!
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/json-1.8.2/lib/json/common.rb:155:in `initialize'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/json-1.8.2/lib/json/common.rb:155:in `new'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/json-1.8.2/lib/json/common.rb:155:in `parse'
     # ./spec/controllers/api/v1/users_controller_spec.rb:20:in `block (3 levels) in <top (required)>'
     # ./spec/support/database_cleaner.rb:10:in `block (3 levels) in <top (required)>'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/database_cleaner-1.4.1/lib/database_cleaner/generic/base.rb:15:in `cleaning'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/database_cleaner-1.4.1/lib/database_cleaner/base.rb:92:in `cleaning'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/database_cleaner-1.4.1/lib/database_cleaner/configuration.rb:86:in `block (2 levels) in cleaning'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/database_cleaner-1.4.1/lib/database_cleaner/configuration.rb:87:in `call'
     # /home/yy/.rvm/gems/ruby-2.1.6/gems/database_cleaner-1.4.1/lib/database_cleaner/configuration.rb:87:in `cleaning'
     # ./spec/support/database_cleaner.rb:9:in `block (2 levels) in <top (required)>'

database_cleaner config

RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

end

#35 楼 @flowerwrong 没有,晚点试下你的代码

文中很多 rake 命令前面为何要加 bundle exe?

#35 楼 @flowerwrong 对你写的 spec 代码做了一些修改:

require 'spec_helper'
require 'awesome_print'

RSpec.describe Api::V1::UsersController, type: :controller do

  + render_views

end

加了一个 render_views, 这样 response.body 就不为空串了。

和请求头加不加 Authorization 没有关系。

render_views 的问题你可以参考 https://www.bountysource.com/issues/9210089-rails-test-log-says-my-templates-were-rendered-when-they-actually-weren-t

#38 楼 @kayakjiang Thx. 😄 But for someone stepping in to a legacy code base, and adding regression tests, it was very surprising. I am very surprising.

太全了 学习了

学习了!

#26 楼 @outman =0=请问要怎么扩展 Grape 让他可以渲染 html?最近也碰到这个问题,但是没有找到解决办法,方便的话望告知~谢谢

好文,涨姿势

一个细节问题:

def reset_auth_token!
  generate_authentication_token
  save # 此处改为: save(validate: false) 会更好一点
end

最近在用 Grape 写 API,发现 rails 中的 request.remote_ip 这种基本功能都没有试验成功。 对 Grape 是又爱又恨。

为什么要用 app/views/api/v1/users/show.json.jbuilder,而不是直接 render json

#45 楼 @gazeldx 你可以在 Nginx 里面配置:proxy_set_header X-Real-IP $remote_addr;
然后在 Grape 里面用 headers['X-Real-Ip'] 就可以取到 remote ip 了。

#42 楼 @lingxueyu 我是先扩展了它的 formatter, 让它支持返回 html, 代码如下仅供参考:

module Grape
  module ContentTypes
    def self.content_types_for(from_settings)
      if from_settings.present?
        from_settings
      else
                ActiveSupport::OrderedHash[
                    :xml,  'application/xml',
                    :serializable_hash, 'application/json',
                    :json, 'application/json',
                    :binary, 'application/octet-stream',
                    :txt,  'text/plain',
                    :html,  'text/html'
            ]

      end
    end
  end
end

module Grape
  module Formatter
    module Html
      class << self
        def call(object, env)
        end
      end
    end
  end
end

Grape::Formatter::Base.formatters({html: Grape::Formatter::Html})

然后写一个专门的 api 接口来处理 html 渲染。模板引擎可以考虑使用 ERB。比如

class Doc < Grape::API 
     format :html
     content_type :html, 'text/html'

     get '/path' do
       #render erb template
     end
end

最后你可以考虑把这个 Api 挂接到你的主 Api 里面。我主要是用来给前端开发人员提供 Api 文档。

#46 楼 @feng88724 使用 jbuilder 模板,主要基于以下三点考虑:

  1. 经典的 MVC 模式,即 Model-View-Controller;

    json.jbuilder 和 html.erb, html.haml 一样作为 view 层

  2. 零代价使用 rails 提供的丰富的 helper 方法;

  3. json.jbuilder 模板有所见即所得的作用,虽然代码可能会有点冗余,但是易于人类阅读;

比如:

json.user do
   json.(@user, :id, :email, :name,  :activated, :admin, :created_at, :updated_at)
end

和需要生成的 json 数据结构是一致的

{ 
   "user":{
      "id":1,
       "email":"[email protected]",
       "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"
   }
}

赞一个!

#47 楼 @outman 非常感谢。很可惜我尝试过后,依然没有成功。

这种 API 的验证应该考虑用 JWT 更好 http://jwt.io/

#52 楼 @wahyd4 看了一下 JWT, 确实不错,赞

我的 log 出现 "Can't verify CSRF token authenticity"

可以直接用 gem 'rails-api', 剔除了 asset-pipeline 和 session 相关的功能。 授权用 jwt 处理,同时本地不需要存储 authentication_token 的。jwt 可以看这个,我现在的项目就是做 REST API 开发 https://ruby-china.org/topics/25080

#55 楼 @yue Rails 剔除 asset-pipeline 和 session 用不了几行代码,jwt 不错,可以把 api authentication 这一块的逻辑标准化。 你说的 本地不需要存储 authentication_token 不太正确,你用 jwt encode 的那个 payload 其实就相当于一个 authentication_token

对于 api 这种需要高并发的 http 请求,建议还是逃离 rails,逃离 grape。基于简单的 rack http server 能直接把 rails 的并发秒成渣。 详情请阅读这篇文章 http://www.madebymarket.com/blog/dev/ruby-web-benchmark-report.html

另外我们自己做基于 eventmachine+rack 的 api framework 能轻易的达到每秒上千的并发。 rails 的并发性能实际上测试下来非常糟糕,差别在一个数量级。

#56 楼 @kayakjiang 准确来说是 user 不需要 authentication_token 的属性,没有数据库的存储。

👍 很完整的示例,从里面学习到不少技巧啊。关于 API 集合分页,如果考虑使用 Link header 的话,推荐 api-pagination

#57 楼 @join 我上一家公司曾经遇到这样一个事故,好几台部署了某个 java 服务的服务器经常内存耗尽导致服务崩溃,开始以为是 rails 导致的因为上面也部署了一些 rails 应用,并且大家习惯上认为 rails 性能低下,所以都去检查 rails 是不是出问题了,后来发现是 java 导致的,事故的原因听其他同事讲是每来一个请求,java 都开一个线程去处理这个请求,但是这个请求后面有些写的很复杂很低效的 sql, 然后这个请求就一直耗着,线程一直挂着,内存也得不到释放,理论上只要机器支持,java 这个并发量可以做到很大,但是在这种情况下,这种所谓的数据上光鲜的大并发不但没有什么用还是有危害的,因为此时并发越多,挂住的线程越多,就会越快把服务器的内存搞光。我看了你提供的链接,说实话我认为这种温室里的性能测试没有什么价值。

另外我们自己做基于 eventmachine+rack 的 api framework 能轻易的达到每秒上千的并发 我很好奇你们是如何测试的,如果在真实的生产环境下你们测试能达到上千的并发,我觉的很了不起,如果你能提供代码 (可以把涉及商业机密的代码去掉) 或者写篇文章介绍下就更好了。

#58 楼 @yue 是的,你写的那篇帖子,是我见到的第一篇有关 jwt 实际应用的帖子,👍,以后可以参考下。

#59 楼 @reyesyang api-pagination 这个东西不错 👍, 我收获也很多 😄

63 楼 已删除

写着写着有点没想通。

class Api::V1::BaseController < ApplicationController
  attr_accessor :current_user
end

module Api
  module V1
    class CinemasController < BaseController
      def index
        @user = current_user  # 此处为什么可以访问父类的current_user
      end
    end
  end
end

我换种方式试了下,不行,求解

class A
  attr_accessor :a
end

class B < A
  def b
    p a
  end
end

A.new.a = 5
B.new.b  # nil
65 楼 已删除

#45 楼 @gazeldx 你可以这样去取 remote_ip,request.env['HTTP_X_REAL_IP'] Grape 内部有一层包装,遇到类似问题,建议查看一下 Grape 的源代码。

之前关注过 Grape 写 API 的性能。与 Rails 相比,确实提升一点,主要提升在 Rails ActionDispatch::Routing 部分。但是,不论 Grape 还是 Rails,在处理 HTTP 请求方面,都很慢!一个 Grape 写的 Hello World API 并发量都很小。

对于创业初期的小项目,应该没有问题。已经有用户量的项目,需要考虑好,上线后的平行扩展,及时升级服务器硬件。

之前做的 AB 并发测试总结,写在一篇博客上了,感兴趣的朋友可以看一下。 PS:第一次冒充测试人员,做并发测试,应该有不少盲点,欢迎提意见。

另外,国外网友总结的一篇性能报告,非常值得一看: BTW:老外测试的硬件条件比较牛,所以,数据只能作为参考。

token 防泄漏是个伪命题啊

#64 楼 @flowerwrong 有点歪楼了。。。


A.new.a = 5 # 生成A实例 赋值成员变量a 然后该实例被抛弃
B.new.b     # 生成B实例 成员变量a尚未赋值 b方法打印a nil

如果测试的话


b = B.new
b.a = 5
b.b  # 5 

非常感谢楼主的分享👍 现在在工作中用apidocjs来写 api 文档,它以注释的形式和代码写在一起,觉得维护起来能更方便一些。

支持一下 好文

#64 楼 @flowerwrong 打个比方,甲和乙是父子关系,甲开了一个健身馆 a, 甲去健身馆 a 锻炼减了 10 斤肉,你不能认为乙减了 10 斤肉,甲减的 10 斤肉和乙没有任何关系,当然乙也可使用甲的健身馆 a 锻炼,也可减 10 斤肉。

#68 楼 @i5ting 也不能说是个伪命题,https 可以防止报文内容被第三方窃取,但是防止不了第三方去猜测,如果 token 很简单,容易被第三方猜出来,这时候加上 httpssss 都无济于事。

#70 楼 @wangyuehong 我个人还是认为使用 markdown 写文档好些,一方面 markdown 对人类很友好,容易学习,另一方面 api 文档不单是文字,还有代码,图片等内容,使用 markdown 可以很容易添加这些内容

#74 楼 @kayakjiang 是的,apidoc 这类的文档工具自由度差很多,选用它最大的原因是为了在改代码的时候能方便的同步更新文档。 但是,如果缺少维护文档意识的话,用再方便的工具也会失去其作用。从这种角度来考虑,用 markdown 是更好的方案。

请问 slate 内的搜索不能支持中文。。。请问有什么办法或者好解决么?

#77 楼 @springles 使用浏览器自带的文本搜索,Meta + F 键

#78 楼 @kayakjiang 看了您的文章,我想用 slate 做我们项目的文档,然后提供给用户,但是不能支持中文搜索就囧了。有空我看看代码:)

#79 楼 @springles 它这个搜索是 js 实现的,你自己改造下吧

83 楼 已删除
  build-an-api-rails-demo  rails --version
Rails 4.2.1

Started GET "/api/v1/users/1" for ::1 at 2015-06-29 21:10:00 +0800

ArgumentError (Invalid request forgery protection method, use :null_session, :exception, or :reset_session):
  app/controllers/api/v1/base_controller.rb:3:in `<class:BaseController>'
  app/controllers/api/v1/base_controller.rb:1:in `<top (required)>'
  app/controllers/api/v1/users_controller.rb:1:in `<top (required)>'

#84 楼 @nuc093 提供下相关的代码,我这边测试了没有问题

@nuc093 有两个问题,第一个问题是拼写错误,把null_session拼成了seesion 另外一个问题,模板文件的位置错了,应该在 views 文件夹下。

@ken @kayakjiang 84#问题解决了。代码已push。

给数据库中已存在的测试用户增加密码和 authentication token 这步出问题了

  build-api-rails git:(master) bundle exe rails c
Loading development environment (Rails 4.2.1)
2.0.0-p481 :001 > User.all.each {|user|
2.0.0-p481 :002 >       user.password = '123123'
2.0.0-p481 :003?>     user.reset_auth_token!
2.0.0-p481 :004?>   }
SyntaxError: /Users/qk/mygithub/build-api-rails/app/models/user.rb:7: syntax error, unexpected keyword_self, expecting '|'
            self.authentication_token = SecureRandom.base64(64)
                ^
/Users/qk/mygithub/build-api-rails/app/models/user.rb:9: syntax error, unexpected keyword_end
/Users/qk/mygithub/build-api-rails/app/models/user.rb:16: syntax error, unexpected end-of-input, expecting keyword_end
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:457:in `load'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:457:in `block in load_file'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:647:in `new_constants_in'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:456:in `load_file'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:354:in `require_or_load'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:494:in `load_missing_constant'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:184:in `const_missing'
    from (irb):1
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/railties-4.2.1/lib/rails/commands/console.rb:110:in `start'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/railties-4.2.1/lib/rails/commands/console.rb:9:in `start'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/railties-4.2.1/lib/rails/commands/commands_tasks.rb:68:in `console'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/railties-4.2.1/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
    from /Users/qk/.rvm/gems/ruby-2.0.0-p481/gems/railties-4.2.1/lib/rails/commands.rb:17:in `<top (required)>'
    from bin/rails:4:in `require'
    from bin/rails:4:in `<main>'

#54 楼 @harris_ruby 我也出现了同样的问题。成功拿到 token,但在 Authenticate User 的时候出错,提示 Can't verify CSRF token authenticity,请教大家该如何调试。

Started PUT "/api/v1/users/1" for 127.0.0.1 at 2015-06-30 15:44:29 +0800
Processing by Api::V1::UsersController#update as */*
  Parameters: {"user"=>{"contact"=>"13800000000"}, "id"=>"1"}
Can't verify CSRF token authenticity
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.6ms | ActiveRecord: 0.0ms)

#89 楼 @idlesong 并没有出错,这只是一个提醒日志:Can't verify CSRF token authenticity, 如果不喜欢这种日志,可以加上

skip_before_filter :verify_authenticity_token

class Api::V1::BaseController < ApplicationController

 + skip_before_filter :verify_authenticity_token

end

为什么遇到一点问题就不能自己 google 搜索一下?

#91 楼 @kayakjiang 感谢,已经定位到问题所在。rails 还不太熟,英语又有点菜,之前看 google 的结果有点雾水,而 API 出错提示太少,感觉有点进展不下去,所以想问问有什么调试方法。 现在 logger 到是 HttpAuthentication 返回了 token,但 options 为空导致的,我再查查是什么原因导致的,有结果再来汇报。

问题解决。 rails4.1.8 中

token, options = ActionController::HttpAuthentication::Token.token_and_options(request)

函数返回 token 和 options 的值同时都赋给了 token,options 为空。升级到 rails4.2.0 问题解决。

#92 楼 @idlesong 我说话比较直接,你不要太介意,在技术论坛里交流,除了瞎扯淡,提供的信息越多越好,这样大家都节约时间,你提供的信息越多,步骤越详细,我们会越认为你是认真的,越会愿意花时间帮你 😄

#93 楼 @idlesong :plus1: 我的 demo 用的是 Rails 4.2.0 版本,所以当时没有出现这个问题

#94 楼 @kayakjiang 不要客气,你的这个教程对我帮助已经很大了。做技术的,肯定能理解凭只言片语帮别人 debug 有多费劲。多谢!

97 楼 已删除

按照文章做了一遍,学到好多干货!感谢楼主!

几个问题和建议:

  • authenticate_user! 定义后,文章中的 user_controller 那段代码中没有写 before_action 不过代码里有写
  • 建议 curl 那里 提醒一下读者把 token 更换成他们自己生成的
  • 文章中有一处 拼写错误app/vies/api/v2/users/show.json.jbuilde 中的vies应为view
  • docs_build.sh 脚本的存放路径应该交代一下 因为看到脚本中有pwd这个名字,如果位置放置不当 可能无法达到效果
  • api 文档中的 index.md 范例里的 json 代码语法高亮部分有转义字符,直接复制可能没法看到语法高亮效果,建议提醒一下读者

#98 楼 @lazybios 不错的建议,我等下看看,谢谢

谢谢楼主 @kayakjiang

发现一处笔误,这段更新用户 name 的路径应为:

app/views/api/v1/users/update.json.jbuilder

顶一下吧

#100 楼 @springwq 已经改过来了,谢谢

#55 楼 @yue authentication_token 这个场景可不可以用 Message Verifier?

105 楼 已删除

最近正在研究 rails 做 API 服务器 ~非常有用~

剛剛試做了一下 不知道為什麼結果一直說是沒有這段路由 這全部都是直接複製貼上的,所以應該會跟範例的程式相同

希望有人可以幫忙解決一下 如果還需要哪個檔案,我都可以截圖

#107 楼 @yuze1995 你试下 /api/v1/users/1.json, 你少写了版本号:v1

@kayakjiang 喔喔 我忘記要加 v1 了 因為第一次試驗時我的路徑沒有那個 v1 的資料夾 可是現在變成這樣了 是我還需要裝什麼東西嗎?

#109 楼 @yuze1995 你访问的这个路径是没有 html 模板生成的。你要在最后加上 json 表明这个请求要返回 json 数据

喔喔 原來如此 可是範例中沒有加 .json 讓我以為會直接判斷回傳型態 可是直接把回傳的型態打在路徑這樣不會不太安全嗎?

#112 楼 @yuze1995 嗯嗯,范例有点问题,我当时是用 curl 做的请求没有出现这种问题。这个和安全无关。

$ curl -i -X POST -d "user[email][email protected]&user[password]=123123" http://localhost:3000/api/v1/sessions拿到用户的 token 的时候报错, ActionController::InvalidAuthenticityToken in Api::V1::SessionsController#create ActionController::InvalidAuthenticityToken

Rails.root: /home/oy/build-an-api-rails-demo

#114 楼 @amrxjks 有时间我试验下

我刚才试验了下没有问题:

curl -i -X POST -d "user[email][email protected]&user[password]=123123" http://localhost:3000/api/v1/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
X-Ratelimit-Limit: 10000
X-Ratelimit-Remaining: 9999
Etag: W/"c0e0519f9c89a846b241a7b0dccd6369"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 22758305-9c86-496d-9223-a76c5828038a
X-Runtime: 0.547032
Vary: Origin
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Tue, 22 Dec 2015 05:09:27 GMT
Content-Length: 150
Connection: Keep-Alive
Set-Cookie: request_method=POST; path=/

{"session":{"id":1,"name":"gg-user","admin":false,"token":"izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w=="}}%  

你可以看下我写的 demo 代码: https://github.com/baya/build-an-api-rails-demo

@kayakjiang 跑通了一遍,很赞;如果要上传图片有什么推荐的方法吗?

#117 楼 @shin 图片和一般的 web form 表单提交文件差不多,没有特别的地方。你可以写一个空的方法接收客户端传过来的 image data, 在方法里打印 image data 的类型等细节,然后根据实际情况写些代码将图片写入到磁盘或者文件服务器。

想请教一下 LZ 为什么第一步里需要 disable 掉 cookie?是有安全性方面的考虑么?

#119 楼 @thxagain 用不着的东西就去掉

rails 5 支持 --api 来创建 rails api 项目,去除了诸如 assets、模板 render 等东西 https://github.com/rails/rails/pull/19832

#121 楼 @gnodiah 嗯,rails 5 有这个功能,我当时写这个帖子也是顺应这个趋势,rails 即可以写 api 也可以写传统的 web 项目,没有必要使用 --api 选项将 rails 的功能限定在 api.

这个是神马情况 请求这个 http://localhost:3000/api/v1/users/1

#123 楼 @brasbug 我自己跑的时候是没有问题的,你这个是不是传错了 user id, 你找一个数据库里实际存在的 user 去测试下

求问,这个在使用 slate 的时候,左侧边的目录始终出不来,这个是啥情况啊?lz 有遇到没有?

~~~~~~~~~~~~~~~~~~~~~~

无视我吧,原来是 google 的 jquery cdn 挂了导致了,换了国内的就好了>_<

感谢 LZ 的分享,提个小问题:

# base_controller.rb
# disable the CSRF token 
protect_from_forgery with: :null_session

这里,既然已经独立出来了一个base_controller,那是否可以直接把 CSRF 防御给关闭了:

skip_before_action :verify_authenticity_token

否则 Log 中每次请求都会出现一个 info 信息,很烦人:

Can't verify CSRF token authenticity

#126 楼 @night_7th 好建议,其实我 demo https://github.com/baya/build-an-api-rails-demo 里已经加了 skip_before_action :verify_authenticity_token, 我在文章里注明下

刚写一篇用 doorkeeper 实现 auth2.0 保护 api 的博客,推荐下:Ruby on Rails 使用 doorkeeper 实现 auth2.0 保护 api 接口

#114 楼 @amrxjks 我也遇到这个问题,后来查看一下代码发现 SessionsController 没有继承 BaseController,-_-!

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

user = user_email && User.find_by(email: user_email) 这个赋值没看懂,user 这样初始化不就是 boolean 类型吗,怎么下一条语句中可以使用 user.authentication_token 取到 token 值?

#131 楼 @wahahaha 你可以做个试验,

res = 1 && 2
res #=> 2

&& 这个符号的计算有下面两种情况

  1. 对左边的对象求 boolean 值,左边的对象的 boolean 值为 true, 返回右边对象
  2. 对左边的对象求 boolean 值,左边的对象的 boolean 值为 false, 返回左边对象

ruby 中只有两个对象的 boolean 值为 false: nil 和 false, 其他对象的 boolean 值都为 true.

user = user_email && User.find_by(email: user_email)

相当于

if user_email
  user = User.find_by(email: user_email)
end

##出错,我在 app/views/api/v1/users 目录下新建了 show.json.builder 文件,

Missing template api/v1/users/show, api/v1/base/show, application/show with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. Searched in: * "/root/build-an-api-rails-demo/app/views"

请教下问题出在哪?

#133 楼 @ecloud 如果你是用浏览器访问,需要加上 .json 的后缀, http://localhost:3000/api/v1/users/1.json

#134 楼 @kayakjiang 谢谢回复,我确实是用的浏览器访问的。

卡了快两天了,求老司机带路

@kayakjiang @rei 新手弄不懂报错,http/1.1 500 也没找到对的解决方法,看到弹一大堆数据出来,人都是懵的,请前辈指教。(弱弱的问一句,这要这么看报错啊?单纯看前面几条就可以了吗?)

#137 楼 @xiao1994 用浏览器访问的话,在 url 后面加 .json, 比如:

/api/v1/users/1.json

别贴图,直接贴异常信息。

#138 楼 @kayakjiang localhost:demo renren120$ curl -i http://localhost:3000/api/v1/users/1.json HTTP/1.1 500 Internal Server Error Content-Type: text/html; charset=utf-8 Content-Length: 138376 X-Web-Console-Session-Id: 14cbf9519193f0475f02f40ecb9a2b9a X-Web-Console-Mount-Point: /__web_console X-Request-Id: ece6582b-14fe-4088-bcfc-8d2b209f7b84 X-Runtime: 0.330466 Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) Date: Thu, 31 Mar 2016 02:16:53 GMT Connection: Keep-Alive 多谢前辈

#138 楼 @kayakjiang 又检查了一遍开发环境,都是正常的新版本

#139 楼 @xiao1994 , 把异常日志贴出来,可以通过 tail -100f log/development.log 查看日志

142 楼 已删除

https://coding.net/u/xiao1994/p/ruby-on-rails/git 麻烦哒,多谢 tail -100f log/development.log又get到技能了,膜拜 怎样才能定位到应用代码中的错误提示啊? 5:53 心态崩崩崩

#143 楼 @xiao1994 ,你给的信息没有什么用,你先把 log/development.log 删除,然后 curl -i http://localhost:3000/api/v1/users/1.json, 注意不要做其他请求,最后把 development.log 的内容贴过来

#144 楼 @kayakjiang Started GET "/api/v1/users/1.json" for ::1 at 2016-04-01 10:29:35 +0800 Processing by Api::V1::UsersController#show as JSON Parameters: {"id"=>"1"} [1m[35mUser Load (0.1ms)[0m SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]] Completed 500 Internal Server Error in 3ms (ActiveRecord: 0.1ms)

ActionView::MissingTemplate (Missing template api/v1/users/show, api/v1/base/show, application/show with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. Searched in:

  • "/Users/renren120/demo/app/views" ): 在别人电脑上好像都能跑,我这就一直报这个错。
146 楼 已删除

#144 楼 @kayakjiang show.json.jbuilder 文件给默认成 rb 文件了!打开文件简介才发现,这两天爬坑爬得,日了。 多谢前辈

#146 楼 @xiao1994 不错,自己爬出坑了

#148 楼 @kayakjiang 嗯嗯,前辈写文章辛苦了

if @user && @user.authenticate(create_params[:password])

authenticate 方法是 bcrypt 这个 gem 包中的方法吗?谢谢解答

#150 楼 @ecloud 是的,

gem 'bcrypt', '~> 3.1.7'
# Schema: User(name:string, password_digest:string)
class User < ActiveRecord::Base
  has_secure_password
end

user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
user.save                                                       # => false, password required
user.password = 'mUc3m00RsqyRe'
user.save                                                       # => false, confirmation doesn't match
user.password_confirmation = 'mUc3m00RsqyRe'
user.save                                                       # => true
user.authenticate('notright')                                   # => false
user.authenticate('mUc3m00RsqyRe')                              # => user
User.find_by(name: 'david').try(:authenticate, 'notright')      # => false
User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
152 楼 已删除
153 楼 已删除
154 楼 已删除

slate 貌似更新了要改 html 才能达到教程的效果。还有 API 调用的第三方是不是有问题啊?还是也更新了? 学到很多东西,真想再创马甲来点赞,rails 一边用一边学还是吃力,我去实战圣经去刷一波等级了。。。 之后 android 调用 api,与数据库和部署服务器,前辈有什么能指点一下的吗?

#155 楼 @xiao1994 我能说的都能通过 google 找到,数据库一般用 mysql 或者 postgresql, 部署一般是 nginx+passenger+cap

#156 楼 @kayakjiang 嗯嗯,谢谢前辈。

写的如此详细,对新手很友好+1

请问用 will_paginate 怎么分页?

slate 在写 API 文档的时候,片段不支持左边栏的中文?

#161 楼 @shin 我看了这个,按照这个在 controller 里面写了,但是用 jq 解析 jbuilder 显示 500 错误

#162 楼 @azpokr 感觉你写错了,我下午刚刚用了一下成功的,你看下我的片段;

class Api::V1::FightpostsController < Api::V1::BaseController
    load_and_authorize_resource except: [:index, :show]

    def index
        user = User.find(params[:user_id])
        fightposts = user.fightposts
        paginate json: fightposts, per_page: 10, status: 200
    end

#162 楼 @azpokr 只要添加这段就行paginate json: actors, per_page: 10

#163 楼 @shin 我按照你的写了一下 显示解析的时候未定义 length 我的 js:

#165 楼 @azpokr 这个情况我不清楚,@finances里面有数据吗?另外我也不太明白你既然用了 active_model_serializers 为啥要用 jbuilder

#166 楼 @shin active_model_serializers 那个是我搞错了,用 jbuilder 的这个方法,我测试接口数据是按分页的,分页有数据的,但是我前端页面显示不出来,前段用<%= will_paginate @finances %>显示报错如图,total_pages 未定义?

#166 楼 @shin 我这个是前端页面 id = search 那部分是返回的 json 数据解析完了的那个部分

#168 楼 @azpokr 1.在 web 里,你在 controller 里有加.paginate(page: params[:page]) 吗?如果有加的话我觉得是你@finances里面没数据!没有数据一般会报 total_pages 未定义

<% unless @finances.empty? %>
  <%= will_paginate @finances %>
<% end %>

#169 楼 @shin 刚才加上之后倒是不报错了,但是 jquery 解析报错了 我解析没有问题啊,之前不分页是能把所有数据读出来的

#170 楼 @azpokr 你另开一个贴吧,问问大家看!

#171 楼 @shin 好的,谢谢前辈!

谢谢楼主分享,学习啦。

#111 楼 @kayakjiang 怎样去除.json 这个后缀呢?有点讨厌。

#174 楼 @sunnyhust2005 如果去掉.json 后缀,相应的你每次请求时需要增加一个 format=json 的参数

每次update_attributes之后都会更新authenticaton_token。更新属性后就又要获取一遍authentication_tokne,是不是太麻烦了。有没有方法可以避免这种情况?

#176 楼 @ecloud , 不会出现这种情况吧,我怎么没有遇到过,

class User < ActiveRecord::Base

  has_secure_password

  has_many :microposts

  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

按道理 before_create 只会对 create 方法起作用,update_attributes 应该不会触发 generate_authentication_token.

要不你试着将 before_create :generate_authentication_token 注释掉,然后创建 user 后,执行 user.generate_authentication_token

或者改下 generate_authentication_token 方法

def generate_authentication_token(force=false)
  # 如果强制或者 authentication_token 是空的则生成 authentication_token
   if force || authentication_token.blank?
    loop do
      self.authentication_token = SecureRandom.base64(64)
      break if !User.find_by(authentication_token: authentication_token)
    end
   end
  end

#177 楼 @kayakjiang 不好意思,之前有事现在才看到。确实是我写的有问题,我用的不是before_create,而是before_save。谢谢这么仔细解答我的问题。

smartepsh [该话题已被删除] 提及了此话题。 07月19日 17:05
smartepsh 使用 Rails 5 创建 API-Only 应用 提及了此话题。 07月20日 13:31

@kayakjiang 你好,我在使用分页的时候,想同时使用 page 和 per_page 参数,但是看记录里,他只能识别第一个参数,而不能识别&后面连接的那个. 请问下如何解决?如果只提交一个参数是可以的. 比如http://localhost:3000/user/1/microposts?per_page=5&page=2每 5 个为 1 页,显示第 2 页,可是 rails 服务器看到的提示只有Parameters: {"per_page"=>"5", "user_id"=>"1"}两个参数。

#181 楼 @smartepsh 你自己多试下,检查下 & 是否正确。

#168 楼 @kayakjiang 谢谢回复,刚才真正的搞定了,应该添加转义符,使用http://localhost:3000/user/1/microposts?per_page=5\&page=2正常了。但是应该是这样吗?

#183 楼 @smartepsh 在浏览器访问应该不用添加转义符,如果是用 curl 在 shell 中访问可能会出现这种情况

#184 楼 @kayakjiang 恩,是在 shell 里。这次知道的就好说了。我用 rails5+serializer 做了一遍你这个例子,感觉差异还是有的...

新人请教下 bundle exe 命令和 --no-assets 参数为何无法识别? 本人 ruby 版本 2.2.4, rails 4.2

@kayakjiang slate 如何在做 api 的版本?比如之前有 v1,后来有 v2 了。

在 session controller 中,

 # 我们使用 jbuilder
      # render(
      #   json: Api::V1::SessionSerializer.new(user, root: false).to_json,
      #   status: 201
      # )```

这段话不是已经注释掉了吗?为什么如果我不加的话,会出现报错呢,加了就好了

请大神,在增加授权那一节一直过不去,有没有一些更详细的解析呢?

#189 楼 @panxiubin 我觉得我写的已经很详细了,你应该首先把你自己遇到的问题描述详细,你说会出现报错,报了什么错误? 你说增加授权过不去,这里又出现了什么错误?实在不行,你把我写的代码 gi clone 到本地跑下就清楚了。 请不要叫我大神,在这个论坛有问题直接提,不需要一些额外的称呼。 https://github.com/baya/build-an-api-rails-demo

403 的报错一直出不来,多谢指教@kayakjiang

@kayakjiang 一直返回的是 401Unauthorized,而不是 403 Forbidden,我这里是哪里错了呢?谢谢

#192 楼 @panxiubin 你贴下日志,403 Forbidden, 可能是你用来发起请求的 user 没有配置权限

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

只有 admin, 或者 user 本人才能更新自己,贴图片等同时把代码和 log 也贴出来,

def update?
    return true if user.admin?
    return true if record.id == user.id
  end
Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/actionpack-4.2.1/lib/action_dispatch/middleware/templates/rescues/_source.erb (4.2ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/actionpack-4.2.1/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (2.0ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/actionpack-4.2.1/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (1.0ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/actionpack-4.2.1/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb within rescues/layout (43.2ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/_markup.html.erb (0.2ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/_inner_console_markup.html.erb within layouts/inlined_string (0.5ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/_prompt_box_markup.html.erb within layouts/inlined_string (0.2ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/style.css.erb within layouts/inlined_string (0.4ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/console.js.erb within layouts/javascript (39.0ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/main.js.erb within layouts/javascript (0.2ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/error_page.js.erb within layouts/javascript (0.3ms)
  Rendered /Users/panxiubin/.rvm/gems/ruby-2.2.0/gems/web-console-2.3.0/lib/web_console/templates/index.html.erb (82.2ms)


Started GET "/api/v1/users/2.json" for ::1 at 2016-11-14 16:30:32 +0800
Processing by Api::V1::UsersController#show as JSON
  Parameters: {"id"=>"2"}
  [1m[36mUser Load (0.1ms)[0m  [1mSELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1[0m  [["id", 2]]
  Rendered api/v1/users/show.json.jbuilder (1.9ms)
Completed 200 OK in 7ms (Views: 5.8ms | ActiveRecord: 0.1ms)


Started PUT "/api/v1/users/2" for ::1 at 2016-11-14 16:37:59 +0800
Processing by Api::V1::UsersController#update as */*
  Parameters: {"id"=>"2"}
Can't verify CSRF token authenticity
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.3ms | ActiveRecord: 0.0ms)


Started PUT "/api/v1/users/2" for ::1 at 2016-11-14 16:38:42 +0800
Processing by Api::V1::UsersController#update as */*
  Parameters: {"id"=>"2"}
Can't verify CSRF token authenticity
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.2ms | ActiveRecord: 0.0ms)
  [1m[36mUser Load (0.1ms)[0m  [1mSELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1[0m
  [1m[35mUser Load (0.2ms)[0m  SELECT "users".* FROM "users"
  [1m[36mUser Load (0.2ms)[0m  [1mSELECT  "users".* FROM "users"  ORDER BY "users"."id" DESC LIMIT 1[0m
  [1m[35mUser Load (0.2ms)[0m  SELECT  "users".* FROM "users"  ORDER BY "users"."id" DESC LIMIT 1


Started PUT "/api/v1/users/2.json" for ::1 at 2016-11-14 17:10:53 +0800
  [1m[36mActiveRecord::SchemaMigration Load (0.1ms)[0m  [1mSELECT "schema_migrations".* FROM "schema_migrations"[0m
Processing by Api::V1::UsersController#update as JSON
  Parameters: {"user"=>{"name"=>"gg-user"}, "id"=>"2"}
  [1m[35mUser Load (0.2ms)[0m  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT 1  [["email", "[email protected]"]]
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 21ms (Views: 3.6ms | ActiveRecord: 0.4ms)


Started PUT "/api/v1/users/2" for ::1 at 2016-11-14 17:11:43 +0800
Processing by Api::V1::UsersController#update as */*
  Parameters: {"id"=>"2"}
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.3ms | ActiveRecord: 0.0ms)


Started PUT "/api/v1/users/2.json" for ::1 at 2016-11-14 17:21:28 +0800
Processing by Api::V1::UsersController#update as JSON
  Parameters: {"user"=>{"name"=>"gg-user"}, "id"=>"2"}
  [1m[36mUser Load (0.1ms)[0m  [1mSELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT 1[0m  [["email", "[email protected]"]]
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 7ms (Views: 0.5ms | ActiveRecord: 0.5ms)


Started PUT "/api/v1/users/2" for ::1 at 2016-11-14 17:24:32 +0800
Processing by Api::V1::UsersController#update as */*
  Parameters: {"user"=>{"name"=>"gg-user"}, "id"=>"2"}
  [1m[35mUser Load (0.1ms)[0m  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT 1  [["email", "[email protected]"]]
  Rendered text template (0.0ms)
Filter chain halted as :authenticate_user! rendered or redirected

@kayakjiang 以上是日志,非常感谢

#194 楼 @panxiubin 401 的话,是你请求的用户不存在或者 token 有问题

#195 楼 @panxiubin 你可以重新请求下 token, 你要检查下你请求的 token 和 email 是否是同一个用户的。

@kayakjiang 好的,可以实现 403 Forbidden 了,非常感谢。

kayakjiang 怎样在技术论坛里对技术问题进行高效沟通 提及了此话题。 11月14日 21:48

请教一下大家 slate 的 API 文档是如何实现的呢?以下是做出来的效果图,以及代码。全部的代码在https://github.com/panxiubin/build-an-api-rails-demo-2 自己找了好几遍还是不知道问题出在哪里。。。

#200 楼 @panxiubin 你是不是没有运行构建文档的脚本:


chmod +x docs_build.sh

 ./docs_build.sh

重新跑了以上两个命令还是不行。@kayakjiang 非常感谢!

感谢楼主的教程 O(∩_∩)O 谢谢,我个人建议在使用 gem 时候附带版本,最好教程中所有 gem 版本都说明一下,由于版本不同,导致有一些变得太快的 gem 现有的使用教程部分失灵,最后一部分 slate 就是这个问题,最新版的 ruby 版本都要 2.4 了,我估计大多数人的不会超过 2.3.。。。还有一个问题,对于 V2 版本的 API 部分,由于前面的访问次数限制导致按照教程命令的访问会 403

感谢楼主的教程,用手机 post 请求不成功,看了楼主的帖子才找到原因。

好文章

MiracleWong 使用 Rails 5 创建 API-Only 应用 提及了此话题。 06月26日 19:57

继续学习!

不知道现在来问还会不会得到楼主解答

rails 5.1.5 使用 jbuilder 的时候 api error render 的部分使用

def api_error(opts = {})
  render body: nil, status: opts[:status]
end
adamshen 求推荐用 rails 做后端 api 开发的教程 提及了此话题。 05月03日 13:54
需要 登录 后方可回复, 如果你还没有账号请 注册新账号