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

mxyzm · 发布于 2015年7月25日 · 最后由 mxyzm 回复于 2015年7月27日 · 1770 次阅读
6059
本帖已被设为精华帖!

本文将介绍: 在 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

共收到 5 条回复
4215
chenge · #1 · 2015年7月25日

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

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

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

16154
hfpp2012 · #2 · 2015年7月25日

赞!

3469
liwei78 · #3 · 2015年7月25日

:plus1:

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

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

1665
ericguo · #4 · 2015年7月27日

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

6059
mxyzm · #5 · 2015年7月27日

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

2107 winnie Rails 角色权限功能实现 中提及了此贴 9月30日 19:31
2 huacnlee 将本帖设为了精华贴 10月06日 11:25
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册