Link Search Menu Expand Document

23. 用戶權限控管

23-1 增加後台管理員權限

目前只要有登入就可以進入後台,接下來實作基本的權限檢查(Authorization)吧,只有管理員才可以進入後台。

基本的原理很簡單,我們在 User model 上新增一個字段標記是不是管理員即可。

執行 rails g migration add_role_to_users role:string 增加一個 role 字串符字段到 users table 上。

在之前的課程中,是用 is_admin:boolean 字段來表示是不是管理員,在這裡改用 role:string,這樣的好處是可以有角色擴充的彈性。

執行 rake db:migrate

編輯 app/controllers/admin_controller.rb,增加權限檢查 current_user 的 role (角色)必須是 admin

  class AdminController < ApplicationController
    protect_from_forgery with: :exception

    before_action :authenticate_user!
+   before_action :require_admin!

    layout "admin"

+   protected

+   def require_admin!
+     if current_user.role != "admin"
+       flash[:alert] = "您的權限不足"
+       redirect_to root_path
+     end
+   end

  end

這時候點選主選單的後臺面板,就會出現:

image

這樣就進不去後台了。

要設定第一個管理員,只能透過 rails console 了。

執行 rails console,依序輸入:

u = User.find_by_email("admin@example.org")
u.role = "admin"
u.save!

這樣 admin@example.org 就有權限可以進入後台管理。

23-2 增加其他角色

role 的設計好處是可以擴充不同角色,讓我們新設計一個 editor 角色:

  • admin 有全部後台權限
  • editor 可以進入後台管理活動,但不能管理用戶

編輯 app/controllers/admin_controller.rb,拿掉本來後台都套用的 before_action :require_admin! 權限檢查。然後新增一個 require_editor! 方法,等會我們得逐個 controllers 一個一個加上權限檢查。

  class AdminController < ApplicationController
    protect_from_forgery with: :exception

    before_action :authenticate_user!
-   before_action :require_admin!

    layout "admin"

    protected

+   def require_editor!
+     if current_user.role != "editor" && current_user.role != "admin"
+       flash[:alert] = "您的權限不足"
+       redirect_to root_path
+     end
+   end

    def require_admin!
      if current_user.role != "admin"
        flash[:alert] = "您的權限不足"
        redirect_to root_path
      end
    end

  end

編輯以下檔案,需要 editor 權限:

  • app/controllers/admin/event_registrations_controller.rb
  • app/controllers/admin/event_tickets_controller.rb
  • app/controllers/admin/events_controller.rb
+  before_action :require_editor!

編輯以下檔案,需要 admin 權限:

  • app/controllers/admin/user_profiles_controller.rb
  • app/controllers/admin/users_controller.rb
  • app/controllers/admin/versions_controller.rb
+  before_action :require_admin!

這樣就完成了,每個 app/controllers/admin/ 目錄下的 controller 都要記得加上權限檢查。

23-3 編輯用戶角色

除了第一個管理員需要進入 rails console 編輯角色之外,在編輯用戶那一頁,我們可以實作一個下拉選單來編輯角色。

image

編輯 app/models/user.rb

  class User < ApplicationRecord

+   ROLES = ["admin", "editor"]

編輯 app/views/admin/users/edit.html.erb

   <div class="form-group">
     <%= f.label :email %>
     <%= f.text_field :email, :class => "form-control" %>
   </div>

+  <div class="form-group">
+    <%= f.label :role %>
+    <%= f.select :role, User::ROLES.map{ |x| [t(x, :scope => "user.role"), x] }, { :include_blank => true }, :class => "form-control" %>
+  </div>

註意這裡多加了 :include_blank => true 參數,這會讓下拉選單多一個空的 nil 空選項。

編輯 config/locales/zh-CN.yml

  "zh-CN":
+   user:
+     role:
+       admin: 超級管理員
+       editor: 活動管理員

編輯 app/controllers/admin/users_controller.rb

   def user_params
-    params.require(:user).permit(:email, :group_ids => [])
+    params.require(:user).permit(:email, :role, :group_ids => [])
   end

這樣就可以編輯用戶的角色了。請編輯任一個用戶變成 editor,然後登出,改用那個帳號登入(假用戶的密碼是 12345678),就可以測試上一節的權限檢查了:可以活動管理,但是點用戶管理會跳出權限不足。

23-4 權限重構

既然 editor 沒有權限可以管理用戶,那麽在主選單上就不應該出現用戶管理的連結,讓我們小小改進一下:

編輯 app/views/layouts/admin.html.erb,根據不同權限決定要不要顯示連結:

  <% if current_user %>
+   <% if current_user.role == "admin" || current_user.role == "editor" %>
      <li class="active"><%= link_to('活動管理', admin_events_path) %></li>
+   <% end %>

+   <% if current_user.role == "admin" %>
      <li><%= link_to('用戶管理', admin_users_path) %></li>
+    <% end %>
  <% end %>

image

不過這個 if 條件跟 admin_controller.rb 裡面的權限檢查條件,其實邏輯是一樣的,也就是 admin 的權限包括 editor 的權限,我們進一步重構一下。

編輯 app/models/user.rb,加上兩個方法判斷是不是 admin 和 editor

+  def is_admin?
+    self.role == "admin"
+  end
+
+  def is_editor?
+    ["admin", "editor"].include?(self.role)  # 如果是 admin 的話,當然也有 editor 的權限
+  end

編輯 app/controllers/admin_controller.rb,改成用 is_admin?is_editor? 方法

  protected

  def require_editor!
-   if current_user.role != "editor" && current_user.role != "admin"
+   unless current_user.is_editor?
      flash[:alert] = "您的權限不足"
      redirect_to root_path
    end
  end

  def require_admin!
-   if current_user.role != "admin"
+   unless current_user.is_admin?
      flash[:alert] = "您的權限不足"
      redirect_to root_path
    end
  end

編輯 app/views/layouts/admin.html.erb,改成用 is_admin?is_editor? 方法

  <% if current_user %>
-   <% if current_user.role == "admin" || current_user.role == "editor" %>
+   <% if current_user.is_editor? %>
      <li class="active"><%= link_to('活動管理', admin_events_path) %></li>
    <% end %>

-   <% if current_user.role == "admin" %>
+   <% if current_user.is_admin? %>
      <li><%= link_to('用戶管理', admin_users_path) %></li>
     <% end %>
  <% end %>

這樣就完成了,透過 User model 的 is_admin?is_editor? 方法集中權限檢查的邏輯,之後如果新增不同子權限,只要改 model 就可以了。

23-5 補充: pundit 和 cancancan

如果權限檢查的邏輯比較複雜,有很多角色而且不同 action 又有不同權限,甚至是不同資料有不同權限,這時候我們可以考慮使用專用的權限檢查 gem,幫助我們組織代碼,也避免忘記加上權限(前幾節的實作,如果忘記加上 before_action 你就忘記檢查到囉)。最知名的有以下兩套:

詳細用法請自行參考文檔。


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