Rails Devise Pundit Rolify 学习笔记

monsterooo · 2019年07月03日 · 4172 次阅读

1. 安装 bundle 和初始化

1.1 devise 初始化

bundle add devise # 安装并添加到Gem
rails generate devise:install # 运行生成器
rails generate devise User # 创建模型
rails db:migrate # 运行迁移任务

# 验证以上步骤是否成功
rails s
# 浏览器访问: http://localhost:3000/users/sign_in 看到登录画面就成功了

1.2 pundit 初始化

bundle add pundit # 安装并添加到Gem
rails g pundit:install # 运行生成器

1.3 rolify 初始化

bundle add rolify # 安装并添加到Gem
rails g rolify Role User # 添加Role和User模型
rails db:migrate # 运行迁移任务

1.4 添加一个 Book 资源用于测试

rails g scaffold Book title:string description:text

2 Rolify 库学习

按照官方文档我们现在可以给一个 User 添加一个角色,在 shell 执行rails c进入 rails 调试模式

u = User.first # 获取第一个用户
u.add_role :admin # 给这个用户添加一个admin角色
u.roles # 可以打印出当前已经存在的角色

下面是根据官方文档的一些用法:

2.1 给 user 添加 role

  • 定义全局 role:

注:resource_type、resource_id 这里是为空表示全局

user = User.find(1)
user.add_role :admin

roles 表数据

id name resource_type resource_id
2 admin
  • 定义 role 作用域给一个资源的实例

注:按照操作指南,Book 类中需要调用resourcify方可为 Book 资源提供role的查询

# 针对Book.first这个实例(id)增加一个对应角色
user = User.find(1)
user.add_role :moderator, Book.first

roles 表数据

id name resource_type resource_id
3 moderator Book 1
  • 定义 role 作用域给一个资源类
user = User.find(1)
user.add_role :moderator, Forum

roles 表数据

id name resource_type resource_id
3 moderator Book

2.2 Role 查询

  • 检查 user 是否有全局 role
user = User.find(1)
user.add_role :admin # 设置一个全局role
user.has_role? :admin
=> true
  • 检查 user 是否具有给定的某一个全局 role
user = User.find(1)
user.add_role :admin
user.add_role :moderator
user.add_role :client

user.has_any_role? :admin, :moderator
=> true
  • 检查 user 是否拥有指定资源的 role
user = User.find(2)
user.add_role :moderator, Forum.first # 设置一个role作用域给一个资源(id)
user.has_role? :moderator, Forum.first
=> true
user.has_role? :moderator, Forum.last
=> false
user.has_role? :moderator
=> false # 返回false是因为moderator是给某个资源的role权限, 不是全局的
user.has_role? :moderator, :any
=> true # 返回true,因为用户至少有一个moderator role
  • 检查 user 是否拥有指定类的 role
user = User.find(3)
user.add_role :moderator, Forum # 设置一个role作用域为Forum类
user.has_role? :moderator, Forum
=> true
user.has_role? :moderator, Forum.first
=> true
user.has_role? :moderator, Forum.last
=> true
user.has_role? :moderator
=> false
user.has_role? :moderator, :any
=> true

如果想要查看更多关于 rolify 的资料请查看官方文档

3 Pundit 学习

首先在你的类中include Pundit,这一般是在ApplicationController中调用

class ApplicationController < ActionController::Base
  include Pundit
end

3.1 Policies(策略)

Pundit 核心聚焦于 policy 类,我们需要将这些类放在app/policies文件夹中。下面是一个简单的 policy 类展示了。它的作用是:如果是管理员或者帖子在未发布状态可以进行更新操作

class PostPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def update?
    user.admin? or not post.published?
  end
end

所有的 Policy 类都是 Ruby 的纯对象,它会做如下几点假设:

  • 它的类名和模型名是相同的名称,只是后缀带有Policy

  • 第一个参数是user,在我们的控制器中 Pundit 将调用current_user方法来检索要发送给此参数的内容

  • 第二个参数是某种model对象,你将会检查user对它是否有权限。这个对象不一定是ActiveRecordActiveModel,它可以是任何东西

  • policy 的类实现了一些查询方法,在什么情况下update?create。一般来说映射到控制器的动作

下面是一个基本的使用示例:

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end

一般来说policy都会继承ApplicationPolicy的默认策略

# app/controllers/post_controller.rb
def update
  @post = Post.find(params[:id])
  authorize @post
  if @post.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end

authorize方法会自动推断实现了PostPolicy的类,然后使用current_user和给定的@post记录。然后动过动作推荐出应该调用那个 Policy 方法。authorize大概会做如下的事情:

unless PostPolicy.new(current_user, @post).update?
  raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
end

如果路由动作名称和 policy 不匹配,可以通过给authorize传递第二个参数来指定验证的方法

def publish
@post = Post.find(params[:id])
authorize @post, :update? # publish的校验使用policy中的update方法
@post.publish!
redirect_to @post
end

如果你没有一个实例来给authorize进行授权,你可以传递一个Class如下所示

# Policy 文件
class PostPolicy < ApplicationPolicy
  def admin_list?
    user.admin?
  end
end
# Controller
def admin_list
  authorize Post # 我们没有特定的Post给authorize验证
  # 验证后的其他操作
end

authorize方法返回传递给他的第一个对象,所以你可以向下面这样链式书写

def show
  @user = authorize User.find(params[:id])
end

通过policy你可以在视图中也可以校验权限,这对于 ui 逻辑的显示特别有用

<% if policy(@post).update? %>
  <%= link_to "Edit post", edit_post_path(@post) %>
<% end %>

3.2 Scopes

如果你需要一个只有特定用户才能访问的视图列表数据,可以使用 Pundit 的 Scope 比如下面这样

class PostPolicy < ApplicationPolicy
  # 其他的正常Policy

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end

  def update?
    user.admin? or not record.published?
  end
end

Pundit 会做如下关于这个类 (Scope) 的一些假设

  • 类的名字为Scope并且它嵌套在我们的 policy 类中
  • 该类的第一个参数是 user,Pundit 将会调用current_user方法来发送到此参数内
  • 第二个参数是某种类型的范围,并且可以执行查询。一般来说它是一个ActiveRecord类或ActiveRecord ::Relation。他和之前的 policy 中的方法完全是不同的
  • 该类需要具有resolve相应方法,它需要返回一些可迭代的结果。如果是ActiveRecord类通常返回的是ActiveRecord::Relation

上面的Scope可以从ApplicationPolicy的 Scope 集成

class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end

  def update?
    user.admin? or not record.published?
  end
end

做好如上的动作之后你可以在你的控制器中使用policy_scope

def index
  # 返回所有的Post或未发布的Post,根据当前用户判断
  @posts = policy_scope(Post)
end

def show
  # 先通过Scope过滤后,在通过传递的id参数进行过滤
  @post = policy_scope(Post).find(params[:id])
end

同样的你也可以在视图中使用它

<% policy_scope(@user.posts).each do |post| %>
  <p><%= link_to post.title, post_path(post) %></p>
<% end %>

3.3 确保使用了 PolicyScope

Pundit 很容易实现我们的程序权限验证,并且 Pundit 引导我们去手动为每个控制器的动作创建权限验证。在这种情况下我们很容忘记给某些控制器动作增加权限。

幸运的是 Pundit 提供了这样的一个辅助校验,它通过增加一个after_action钩子,让没有调用authorize的动作抛出异常。下面来看看怎么使用的吧。

class ApplicationController < ActionController::Base
  include Pundit
  # 通过钩子调用 Pundit 内置的 verify_authorized方法
  after_action :verify_authorized
end

同样的 Pundit 还提供了verify_policy_scoped去校验控制器动作是否调用policy_scope

class ApplicationController < ActionController::Base
  include Pundit
  after_action :verify_authorized, except: :index
  after_action :verify_policy_scoped, only: :index
end

暂时就这么多了哈,这里补充一个我练习的仓库地址。

总结:学习了 Devise、Pundit、Rolify 三个库的基本使用方法。我们可以给我们的程序快速加入用户角色资源访问控制。

以上的知识点知识我查看和学习文档时所做的记录,如果无法满足你的好奇心请查看对应的 Github 仓库。

感谢收看

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