Rails 权限设计 - Role-based Authorization with Pundit

mxyzm · 2015年07月25日 · 最后由 wuyuedefeng-github 回复于 2019年05月04日 · 6130 次阅读
本帖已被管理员设置为精华贴

本文将介绍:在 Rails 应用中,如何使用 Pundit 实现一个基于角色的授权系统。

引入角色是为了让用户可以动态管理权限,让权限点以集合的形式分配给用户,如果没有这样的需求,可以考虑其他简单的方案。

数据模型

+------+          +------+          +------------+
|      |  N    N  |      |  1    N  |            |
| User |<-------->| Role |<-------->| Permission |
|      |          |      |          |            |
+------+          +------+          +------------+

User 和 Role 是多对多的关系,Role 和 Permission 是一对多的关系。Permission 的定义包含两个字段:actionresource, 分别与 Rails 的 Controller Action 和 Model 对应。

Model 关系定义:

class User < ActiveRecord::Base
  has_and_belongs_to_many :roles
  has_many :permissions, through: :roles
end
class Role < ActiveRecord::Base
  has_many :permissions, dependent: :destroy
  has_and_belongs_to_many :users
end
class Permission < ActiveRecord::Base
  belongs_to :role
  has_many :users, through: :role
end

权限定义

为了方便在角色管理时能够列出可选择的权限点,权限点的定义需要通过某种方式存储起来:

class ApplicationPolicy
  class << self
    def actions
      @actions ||= []
    end

    def permit(action_or_actions)
      acts = Array(action_or_actions).collect(&:to_s)
      acts.each do |act|
        define_method("#{act}?") { can? act }
      end
      actions.concat(acts)
    end
  end

  private

  def can?(action)
    permission = {
      action: action,
      resource: record.is_a?(Class) ? record.name : record.class.name
    }
    user.permissions.exists?(permission)
  end
end

ApplicationPolicy 里定义一个 permit 方法 (类方法) 用来定义和保存权限点,can? 方法用来做权限检查。

然后就可以像这样声明权限点:

class ResourcePolicy < ApplicationPolicy
  permit [:read, :create, :update, :destroy]
end

这些 Action 就会被保存到 ResourcePolicy.actions 里。

另外还需要两个方法 policiesresource:

class ApplicationPolicy
  class << self
    def policies
      @policies ||= Dir.chdir(Rails.root.join('app/policies')) do
        Dir['**/*_policy.rb'].collect do |file|
          file.chomp('.rb').camelize.constantize unless file == File.basename(__FILE__)
        end.compact
      end
    end

    def resource
      name.chomp('Policy')
    end
  end
end

分别用来获取所有的 Policy 和 每个 Policy 对应的 resource (这两个方法是通过简单的命名规则实现的,灵活性会差一点).

角色与权限

在角色管理中,可以像这样列出所有可选择的权限点:

<% ApplicationPolicy.policies.each do |policy| %>
  <% resource = policy.resource %>
  <div>
    <span><%= resource %></span>
    <% policy.actions.each do |action| %>
      <% checked = role.permissions.exists?(action: action, resource: resource) %>
      <% value = "#{action}##{resource}" %>
      <%= f.check_box :permissions, { multiple: true, checked: checked }, value, nil %>
      <%= f.label :permissions, value, value: value %>
    <% end %>
  </div>
<% end %>

角色与用户

在用户管理中,可以这样为用户指定角色:

<div>
  <%= f.label :roles %><br />
  <%= f.collection_check_boxes :role_ids, Role.all, :id, :name %>
</div>

参考项目

这个系统的完整实现请参考此项目 (mxyzm/oh_my_user) 的后台管理部分。

参考资料

原文地址:http://www.xyyz.me/2015/07/24/role-based-authorization-with-pundit.html

我最近用 cancan,似乎没那么复杂。

只需要 user 有一个 role 字段就可以了。然后根据 role 定义权限,在页面里判断权限。

一个 user 只有一个 role,估计大部分情况都可以的。

:plus1:

如果不是需要经常改动用户的角色,以及增删角色,我也是只给 user 加一个 role:string 的字段,然后用 cancancan 定义就可以了。

如果是 CMS,ERP 或 CRM,就需要楼主的方法了。

cancancan 最大的问题是,如果你突然准备引入一个数据表让用户配某些访问权限,那么 ability.rb 里面写的 where 查询会在每一次 page refresh 时跑一遍,这样,很不好。。不过,我还是继续在用。。。

#4 楼 @ericguo 既然用户能够动态管理权限,在做权限检查时就需要拿到最新的数据,所以数据库查询是不可避免的。但是 cancancan 把权限点都定义在一个地方,而且是在 initialize 方法里,所以可能会带来不必要的查询。

winnie Rails 角色权限功能实现 提及了此话题。 09月30日 19:31
huacnlee 将本帖设为了精华贴。 10月06日 11:25

刚刚照这篇文章在 ActiveAdmin 做的后台里实现了 RBAC 权限管理,感谢😉 😉

权限和角色难道不应该是多对多的关系吗

需要 登录 后方可回复, 如果你还没有账号请 注册新账号