Link Search Menu Expand Document

4. 實作認證 API

4-1 目標

上一章的 API 操作都不需要任何認證,接下來我們想要多加一個功能來示範需要認證的情況:

  • 使用者可以在網頁上註冊、登入,拿到 API Key
  • 如果在有登入的情況下進行訂票的話,則可以查詢該用戶下的所有訂票

本章會實作的 API 是查詢該用戶的所有訂票:

GET /api/v1/reservations

4-2 裝 Devise 產生 User Model

編輯 Gemfile 加上 gem "devise"

執行 bundle,然後重啟伺服器

執行 rails g devise:install

執行 rails g devise user

執行 rake db:migrate

執行 rails g controller welcome

新增 app/views/welcome/index.html.erb 檔案

  <h2>訂票系統</h2>

編輯 config/routes.rb,插入一行:


+   root "welcome#index"

編輯 layout/application.html.erb,插入:


 <body>
+  <% if current_user %>
+     <%= link_to('登出', destroy_user_session_path, :method => :delete) %>
+    <%= link_to('修改密碼', edit_registration_path(:user)) %>
+  <% else %>
+    <%= link_to('註冊', new_registration_path(:user)) %> |
+    <%= link_to('登入', new_session_path(:user)) %>
+   <% end %>

...(略)

編輯 app/models/user.rb,加上 reservations 關聯


 class User < ApplicationRecord

+ has_many :reservations

...(略)

編輯 app/models/reservation.rb,加上 user 關聯


 class Reservation < ApplicationRecord

+ belongs_to :user

...(略)

4-3 產生 API 用的 token

我們在第二章使用天氣 API 時,會用到 api key 來做憑證。這裡我們也想要一樣的機制。 首先會新增一個字段 authentication_token 字段(這裡命名成 token,跟 API Key 是一樣意思),並且亂數產生一個憑證:

執行 rails g migration add_token_to_users

修改這個 migration,內容如下

 class AddTokenToUsers < ActiveRecord::Migration
   def change
+    add_column :users, :authentication_token, :string
+    add_index :users, :authentication_token, :unique => true
+
+    User.find_each do |u|
+      puts "generate user #{u.id} token"
+      u.generate_authentication_token
+      u.save!
+    end
   end
 end

修改 app/models/user.rb 加上 generate_authentication_token 方法:

  class User < ApplicationRecord

+   before_create :generate_authentication_token
+
+   def generate_authentication_token
+     self.authentication_token = Devise.friendly_token
+   end

  ...(略)
  end

然後執行 rake db:migrate

編輯 app/views/welcome/index.html.erb

   <h2>訂票系統</h2>

+  <% if current_user %>
+    <p>已經登入:你的 API token 是 <code><%= current_user.authentication_token  %></code></p>
+  <% else %>
+    <p>尚未登入</p>
+  <% end %>

瀏覽 http://localhost:3000 並註冊一個帳號,就可以在畫面上看到該用戶的 API token 了。

image

4-4 設置 current_user

接著我們在 ApiController 上實作 before_action :authenticate_user_from_token! 方法,如果客戶端的 HTTP request 請求有帶 auth_token 參數的話,就會進行登入(但這裡沒有強制一定要登入):


 class ApiController < ActionController::Base

+  before_action :authenticate_user_from_token!
+
+  def authenticate_user_from_token!
+
+    if params[:auth_token].present?
+      user = User.find_by_authentication_token( params[:auth_token] )
+
+      # sign_in 是 Devise 的方法,會設定好 current_user
+      sign_in(user, store: false) if user
+    end
+  end

 end

只要呼叫 API 的時候,有多帶 auth_token,就會設定好 current_user

4-5 修改訂票 API

修改 app/controller/api/v1/reservations_controller.rbcreate 方法,插入一行 @reservation.user = current_user


 def create
   @train = Train.find_by_number!( params[:train_number] )
   @reservation = Reservation.new( :train_id => @train.id,
                                   :seat_number => params[:seat_number],
                                   :customer_name => params[:customer_name],
                                   :customer_phone => params[:customer_ phone] )

+  @reservation.user = current_user

   if @reservation.save
     render :json => { :booking_code => @reservation.booking_code,
                       :reservation_url =>  api_v1_reservation_url(@reservation.booking_code) }
   else
     render :json => { :message => "訂票失敗", :errors => @reservation.errors  }, :status => 400
   end
 end

這樣如果是登入的情況,這張定票就會關聯到該用戶。

4-6 可以查詢所有我的訂票

修改 config/routes.rb


 namespace :api, :defaults => { :format => :json } do
   namespace :v1 do
+     get "/reservations" => "reservations#index", :as => :reservations
     # ...(略)

修改 app/controller/api/v1/reservations_controller.rb,新增 index 方法:


 class Api::V1::ReservationsController < ApiController

+  before_action :authenticate_user!, :only => [:index] # 這會檢查 index +這個操作一定要登入
+
+  def index
+    @reservations = current_user.reservations
+
+    render :json => {
+      :data => @reservations.map { |reservation|
+        {
+          :booking_code => reservation.booking_code,
+          :train_number => reservation.train.number,
+          :seat_number => reservation.seat_number,
+          :customer_name => reservation.customer_name,
+          :customer_phone => reservation.customer_phone
+        }
+      }
+    }
+  end

  # ...(略)
 end

用 Postman 進行測試,首先新增訂票,記得多傳 auth_token 參數表示這是登入的用戶:

image

接著查詢

image

GET 方法是沒有 HTTP Body 的,它的參數會直接接在網址後面。

4-7 解說

對 API 客戶端來說,所有需要登入的操作,都必須帶入 auth_token 這個參數,這樣伺服器才能識別是哪一個使用者。你可能會覺得為什麽需要這樣的 api key 的機制,每個用戶不是已經有帳號密碼了嗎? 為什麽不能每個操作乾脆帶著帳號密碼參數即可?

這是因為用 api key 的機制,我們可以:

  • 安全性,亂數產生的強度比密碼高,甚至可以設計有效時間
  • 獨立性,使用者改密碼不會影響 api key,這樣客戶端就不需要重新設定過

Copyright © 2010-2021 Wen-Tien Chang All Rights Reserved.