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 了。
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.rb
的 create
方法,插入一行 @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
參數表示這是登入的用戶:
接著查詢
GET 方法是沒有 HTTP Body 的,它的參數會直接接在網址後面。
4-7 解說
對 API 客戶端來說,所有需要登入的操作,都必須帶入 auth_token
這個參數,這樣伺服器才能識別是哪一個使用者。你可能會覺得為什麽需要這樣的 api key 的機制,每個用戶不是已經有帳號密碼了嗎? 為什麽不能每個操作乾脆帶著帳號密碼參數即可?
這是因為用 api key 的機制,我們可以:
- 安全性,亂數產生的強度比密碼高,甚至可以設計有效時間
- 獨立性,使用者改密碼不會影響 api key,這樣客戶端就不需要重新設定過