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
按照官方文档我们现在可以给一个 User 添加一个角色,在 shell 执行rails c
进入 rails 调试模式
u = User.first # 获取第一个用户
u.add_role :admin # 给这个用户添加一个admin角色
u.roles # 可以打印出当前已经存在的角色
下面是根据官方文档的一些用法:
注:resource_type、resource_id 这里是为空表示全局
user = User.find(1)
user.add_role :admin
roles 表数据
id | name | resource_type | resource_id |
---|---|---|---|
2 | admin |
注:按照操作指南,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 |
user = User.find(1)
user.add_role :moderator, Forum
roles 表数据
id | name | resource_type | resource_id |
---|---|---|---|
3 | moderator | Book |
user = User.find(1)
user.add_role :admin # 设置一个全局role
user.has_role? :admin
=> true
user = User.find(1)
user.add_role :admin
user.add_role :moderator
user.add_role :client
user.has_any_role? :admin, :moderator
=> true
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 = 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 的资料请查看官方文档
首先在你的类中include Pundit
,这一般是在ApplicationController
中调用
class ApplicationController < ActionController::Base
include Pundit
end
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
对它是否有权限。这个对象不一定是ActiveRecord
或ActiveModel
,它可以是任何东西
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 %>
如果你需要一个只有特定用户才能访问的视图列表数据,可以使用 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 类中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 %>
Policy
和 Scope
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 仓库。
感谢收看