Skip to content

中间件与鉴权

添加AutoJwt中间件

中间件可以存在于路由和控制器之间,也可以是在响应返回给用户之间。

添加自己的中间件,详细见Rail官网。也可以使用官网的中间件,执行bin/rails middleware就可以看到rails自己的中间件。若是要自己创建中间件则需要在config/application.rb中添加config.middleware.use AutoJwt,并导入中间件文件位置require_relative '../lib/auto_jwt'

创建自己的中间件,创建文件lib/auto_jwt.rb,使用中间件后修改mes_controller.rb。详细内容见链接

ruby
class AutoJwt
  def initialize(app)
    @app = app
  end
  def call(env)
    header = env['HTTP_AUTHORIZATION']
    jwt = header.split(' ')[1] rescue ''
    payload = JWT.decode jwt, Rails.application.credentials.hmac_secret, true, { algorithm: 'HS256' } rescue nil
    env['current_user_id'] = payload[0]['user_id'] rescue nil
    @status, @headers, @response = @app.call(env)
    [@status, @headers, @response]
  end
end
ruby
class Api::V1::MesController < ApplicationController
  def show
    user_id = request.env['current_user_id'] rescue nil
    # get user from user_id
    user = User.find user_id
    return head 404 if user.nil?
    # render user
    render json: { resource: user }
  end
end

重启服务bin/rails server,curl一下端口curl http://127.0.0.1:3000,可以发现返回的和之前一样。

创建获取账目接口

单独执行文件的第几行测试,rspec spec/requests/item_spec.r:5,详细内容见链接

创建测试用例(按时间筛选)。

ruby
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "获取账目" do
    it "分页" do
      11.times { Item.create amount: 100 }
      expect(Item.count).to eq 11
      get '/api/v1/items'
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 10
      get '/api/v1/items?page=2'
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 1
    end
    it "按时间筛选" do
      item1 = Item.create amount: 100, created_at: Time.new(2018, 1, 2)
      item2 = Item.create amount: 100, created_at: Time.new(2018, 1, 2)
      item3 = Item.create amount: 100, created_at: Time.new(2019, 1, 1)
      get '/api/v1/items?created_after=2018-01-01&created_before=2018-01-03'
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 2
      expect(json['resources'][0]['id']).to eq item1.id
      expect(json['resources'][1]['id']).to eq item2.id
    end
  end
  describe "create" do
    it "can create an item" do
      expect {
        post '/api/v1/items', params: { amount: 99 }
      }.to change { Item.count }.by 1
      # by是否增1
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources']['id']).to be_an(Numeric)
      expect(json['resources']['amount']).to eq 99
    end
  end
end

修改获取账目接口,带入查询时间的范围,但是有边界值的问题,还需要处理。

ruby
class Api::V1::ItemsController < ApplicationController
    def index
        items = Item.where({created_at: params[:created_after]..params[:created_before]})
            .page(params[:page])
        render json: { resources: items, pager: {
            page: params[:page],
            per_page: 100,
            count: Item.count
        } }
    end
    def create
        item = Item.new amount: params[:amount]
        if item.save
            render json: { resources: item }
        else
            render json: { errors: item.errors }
        end
    end
end

时区导致测试失败

主要是因为有时区这个问题,创建时间的时候可以设置为Time.new(2018, 1, 1, 0, 0, 0, "Z")Time.new(2018, 1, 1, 0, 0, 0, "+00:00")或直接写死时间'2018-01-01'。修改时区的问题后在新增其他测试用例。详细内容见链接

ruby
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "获取账目" do
    it "分页" do
      11.times { Item.create amount: 100 }
      expect(Item.count).to eq 11
      get '/api/v1/items'
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 10
      get '/api/v1/items?page=2'
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 1
    end
    it "按时间筛选" do
      item1 = Item.create amount: 100, created_at: '2018-01-02'
      item2 = Item.create amount: 100, created_at: '2018-01-02'
      item3 = Item.create amount: 100, created_at: '2019-01-01'
      get '/api/v1/items?created_after=2018-01-01&created_before=2018-01-03'
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 2
      expect(json['resources'][0]['id']).to eq item1.id
      expect(json['resources'][1]['id']).to eq item2.id
    end
  end
  it "按时间筛选(边界条件)" do
    item1 = Item.create amount: 100, created_at: Time.new(2018, 1, 1, 0, 0, 0, "Z")
    get '/api/v1/items?created_after=2018-01-01&created_before=2018-01-02'
    expect(response).to have_http_status 200
    json = JSON.parse(response.body)
    expect(json['resources'].size).to eq 1
    expect(json['resources'][0]['id']).to eq item1.id
  end
  it "按时间筛选(边界条件2)" do
    item1 = Item.create amount: 100, created_at: '2018-01-01'
    item2 = Item.create amount: 100, created_at: '2017-01-01'
    get '/api/v1/items?created_after=2018-01-01'
    expect(response).to have_http_status 200
    json = JSON.parse(response.body)
    expect(json['resources'].size).to eq 1
    expect(json['resources'][0]['id']).to eq item1.id
  end
  it "按时间筛选(边界条件2)" do
    item1 = Item.create amount: 100, created_at: '2018-01-01'
    item2 = Item.create amount: 100, created_at: '2019-01-01'
    get '/api/v1/items?created_before=2018-01-02'
    expect(response).to have_http_status 200
    json = JSON.parse(response.body)
    expect(json['resources'].size).to eq 1
    expect(json['resources'][0]['id']).to eq item1.id
  end
  describe "create" do
    it "can create an item" do
      expect {
        post '/api/v1/items', params: { amount: 99 }
      }.to change { Item.count }.by 1
      # by是否增1
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources']['id']).to be_an(Numeric)
      expect(json['resources']['amount']).to eq 99
    end
  end
end

完善get/api/v1/items的文档

修改控制器的当前页面的默认值。

ruby
class Api::V1::ItemsController < ApplicationController
    def index
        items = Item.where({created_at: params[:created_after]..params[:created_before]})
            .page(params[:page])
        render json: { resources: items, pager: {
            page: params[:page] || 1,
            per_page: Item.default_per_page,
            count: Item.count
        } }
    end
    def create
        item = Item.new amount: params[:amount]
        if item.save
            render json: { resources: item }
        else
            render json: { errors: item.errors }
        end
    end
end

编写接口测试。

ruby
require 'rails_helper'
require 'rspec_api_documentation/dsl'

resource "验证码" do
  get "/api/v1/items" do
    parameter :page, '页码'
    parameter :created_after, '创建时间起点(筛选条件)'
    parameter :created_before, '创建时间终点(筛选条件)'
    with_options :scope => :resources do
      response_field :id, 'ID'
      response_field :amount, '金额(单位:分)'
    end
    let(:created_after) { '2020-10-10' }
    let(:created_before) { '2020-11-11' }
    example "获取账目" do
      	# 执行10次创建item的例子
        11.times do Item.create amount: 100, created_at: '2020-10-30' end
        do_request
        expect(status).to eq 200
        json = JSON.parse response_body
        expect(json['resources'].size).to eq 10
    end
  end
end

鉴权

由于目前没有将用户之前进行区分,需要进行分离。在user的model层上定义两个方法,存取jwt,详细内容见链接

ruby
class User < ApplicationRecord
    validates :email, presence: true

    def generate_jwt
        payload = { user_id: self.id }
        JWT.encode payload, Rails.application.credentials.hmac_secret, 'HS256'
    end

    def generate_auth_header
        {Authorization: "Bearer #{self.generate_jwt}"}
    end
end

session_controller中就可以直接使用方法,直接挂在user上。

ruby
require 'jwt'
class Api::V1::SessionsController < ApplicationController
  def create
    # 如果测试环境
    if Rails.env.test?
      return render status: :unauthorized if params[:code] != '123456'
    else 
      canSignin = ValidationCode.exists? email: params[:email], code: params[:code], used_at: nil
      return render status: :unauthorized unless canSignin
    end
    user = User.find_by_email params[:email]
    if user.nil?
      render status: :not_found, json: {error: '用户不存在'}
    else
      render status: :ok, json: { jwt: user.generate_jwt }
    end
    
  end
end

并在items_controller.rb中加上鉴权判断,若请求头参数没有带上当前的user_id则返回401。

ruby
class Api::V1::ItemsController < ApplicationController
    def index
        current_user_id = request.env['current_user_id']
        return head :unauthorized if current_user_id.nil?
        items = Item.where({user_id: current_user_id})
            .where({created_at: params[:created_after]..params[:created_before]})
            .page(params[:page])
        render json: { resources: items, pager: {
            page: params[:page] || 1,
            per_page: Item.default_per_page,
            count: Item.count
        } }
    end
    def create
        item = Item.new amount: params[:amount]
        if item.save
            render json: { resources: item }
        else
            render json: { errors: item.errors }
        end
    end
end

重新编写测试用例,然后在终端执行rspec spec/requests/items_spec.rb

ruby
require 'rails_helper'

RSpec.describe "Items", type: :request do
  describe "获取账目" do
    it "分页,未登录" do
      user1 = User.create email: '1@qq.com'
      user2 = User.create email: '2@qq.com'
      11.times { Item.create amount: 100, user_id: user1 }
      11.times { Item.create amount: 100, user_id: user2 }
      get '/api/v1/items'
      expect(response).to have_http_status 401
    end
    it "分页" do
      user1 = User.create email: '1@qq.com'
      user2 = User.create email: '2@qq.com'
      11.times { Item.create amount: 100, user_id: user1.id }
      11.times { Item.create amount: 100, user_id: user2.id }

      get '/api/v1/items', headers: user1.generate_auth_header
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 10
      get '/api/v1/items?page=2', headers: user1.generate_auth_header
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 1
    end
    it "按时间筛选" do
      user1 = User.create email: '1@qq.com'
      item1 = Item.create amount: 100, created_at: '2018-01-02', user_id: user1.id
      item2 = Item.create amount: 100, created_at: '2018-01-02', user_id: user1.id
      item3 = Item.create amount: 100, created_at: '2019-01-01', user_id: user1.id
      get '/api/v1/items?created_after=2018-01-01&created_before=2018-01-03', headers: user1.generate_auth_header
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 2
      expect(json['resources'][0]['id']).to eq item1.id
      expect(json['resources'][1]['id']).to eq item2.id
    end
    it "按时间筛选(边界条件)" do
      user1 = User.create email: '1@qq.com'
      item1 = Item.create amount: 100, created_at: Time.new(2018, 1, 1, 0, 0, 0, "Z"), user_id: user1.id
      get '/api/v1/items?created_after=2018-01-01&created_before=2018-01-02', headers: user1.generate_auth_header
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 1
      expect(json['resources'][0]['id']).to eq item1.id
    end
    it "按时间筛选(边界条件2)" do
      user1 = User.create email: '1@qq.com'
      item1 = Item.create amount: 100, created_at: '2018-01-01', user_id: user1.id
      item2 = Item.create amount: 100, created_at: '2017-01-01', user_id: user1.id
      get '/api/v1/items?created_after=2018-01-01', headers: user1.generate_auth_header
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 1
      expect(json['resources'][0]['id']).to eq item1.id
    end
    it "按时间筛选(边界条件3)" do
      user1 = User.create email: '1@qq.com'    
      item1 = Item.create amount: 100, created_at: '2018-01-01', user_id: user1.id
      item2 = Item.create amount: 100, created_at: '2019-01-01', user_id: user1.id
      get '/api/v1/items?created_before=2018-01-02', headers: user1.generate_auth_header
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources'].size).to eq 1
      expect(json['resources'][0]['id']).to eq item1.id
    end
  end
  describe "create" do
    it "can create an item" do
      expect {
        post '/api/v1/items', params: { amount: 99 }
      }.to change { Item.count }.by 1
      # by是否增1
      expect(response).to have_http_status 200
      json = JSON.parse(response.body)
      expect(json['resources']['id']).to be_an(Numeric)
      expect(json['resources']['amount']).to eq 99
    end
  end
end

重构文档测试代码

重构API文档后,可以执行bin/rake docs:generate,然后启动http-server查看文档,执行http-server doc/api,详细内容见链接

ruby
require 'rails_helper'
require 'rspec_api_documentation/dsl'

resource "验证码" do
  get "/api/v1/items" do
    # 验证方式:使用基础验证, 值为auth
    authentication :basic, :auth
    parameter :page, '页码'
    parameter :created_after, '创建时间起点(筛选条件)'
    parameter :created_before, '创建时间终点(筛选条件)'
    with_options :scope => :resources do
      response_field :id, 'ID'
      response_field :amount, '金额(单位:分)'
    end
    let(:created_after) { '2020-10-10' }
    let(:created_before) { '2020-11-11' }
    let(:current_user) { User.create email: '1@qq.com' }
    let(:auth) { "Bearer #{current_user.generate_jwt}" }
    example "获取账目" do
        11.times do Item.create amount: 100, created_at: '2020-10-30',user_id: current_user.id end
        do_request
        expect(status).to eq 200
        json = JSON.parse response_body
        expect(json['resources'].size).to eq 10
    end
  end
end