Cancan权限角色设计的最佳实践
简介
权限存取设计是在开发 Application 中相当棘手的问题。
在网站开始建设的初期,通常这样的问题并不会浮现,毕竟一般人的需求大半只会有 user 和 admin 两种角色。但是随著网站演化,更多的业务需求浮现,第三种角色的出现,通常就会把原本干净的 code 弄得肮脏不堪。
多种角色的权限设计难题
当只有 user 和 admin 的情况下,你可以在 view 里面单纯的做出这样的设计
<% if user.is_admin ? %>
<%= link_to("Admin Pannel", admin_panel_path ) %>
<% end %>
并且在 controller 里面加上权限判断
class Admin::ArticleController < ApplicationController
before_filter :require_is_admin
end
但一段时间之后,出现这样的需求:
- 使用者可以被设定为「editor」
- 拥有「editor」角色的使用者,可以进入 admin 后台发表、编辑文章
- 拥有「edtior」角色的使用者,进入 admin 后台内的活动范围仅限缩在文章后台内
- 拥有「edtior」角色的使用者,进入 admin 后台内,不可以看到其他后台选项。
身为开发者的你,要如何在现有后台内加入这样的设计?
不用实际动手写也知道,若如以往使用 if / else 的设计,Helper / Controller / View 铁定变成一团血肉模糊。
抱怨不能解决问题,但世界上是否存在干净的解答?
答案就是:「Rule Engine」。
Rule-engine based authorization library: Cancan
「针对多种条件执行多种动作」,此类的需求,无论是使用 if / else,甚至是 case when,架构还是不免会一团混乱。与其承袭旧思路,不如启用新想法 - 「Rule Engine」:预先设计撰写一套逻辑规则引擎,而后程序针对预设的规则进行逻辑判断后执行。
而「角色权限」的设计需求上,正特别适合用 Rule Engine 这样的观念去建构。Rails 中的授权库(authorization library) cancan 正是以Rule Engine作为基础。
Cancan的特点1: 接口简单,其把权限判定逻辑从 Helper / Controller / View 中, 移到 app/models/ability.rb,从而实现
- View 只需要判断是否可以执行动作,而不必问是否有权限
<% if can? :update, @article %>
<%= link_to "Edit", edit_article_path(@article) %>
<% end %>
- Controller 无需手动判断权限
class ArticlesController < ApplicationController
authorize_resource
def show
# @article is already authorized
end
end
实际上,权限判断在view和controller通常是雷同的,分散处理常常故此失彼。所以,Cancan的第二个特点是: 权限中心化管理, 即将权限存取,则全交给 app/models/ability.rb 去判断处理。
class Ability
include CanCan::Ability
def initialize(user)
if user.blank?
# not logged in
cannot :manage, :all
basic_read_only
elsif user.has_role?(:admin)
# admin
can :manage, :all
elsif user.has_role?(:member)
can :create, Topic
can :update, Topic do |topic|
(topic.user_id == user.id)
end
can :destroy, Topic do |topic|
(topic.user_id == user.id)
end
basic_read_only
else
# banned or unknown situation
cannot :manage, :all
basic_read_only
end
end
protected
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
end
使用Cancan 的限制:RESTful controller (resource)
初学者不太理解 cancan 两个接口函数:load_and_authorize_resource
、authorize_resource
此外,cancan 也没在Readme中说明其架构的限制,即:
- 必须为 RESTful resource (cancan 直接假设了你一定使用 RESTful,毕竟这年头谁还在写 non-RESTful …?)
- resource 必须与 Controller 同名(@article 与 ArticlesController)
这两条限制,也是Rails默认的配置,所以,使用过 cancan 的人,大概都「猜到」规则好像是这样?
其实,其source code 里面就写的很清楚
load_and_authorize_resource
load_and_authorized_resource
做了两件事:
def load_and_authorize_resource
load_resource
authorize_resource
end
load_resource
authorize_resource
load_resource
是load_resource_instance
的符号别名(:loard_resource => load_resource_instance
)
def load_resource_instance
if !parent? && new_actions.include?(@params[:action].to_sym)
build_resource
elsif id_param || @options[:singleton]
find_resource
end
end
okay,这段代码的意思是如果你在 Controller 里面下加入load_resource
,cancan 会自作聪明的帮你 自动 在每一个 action中加一个instance 。
class ArticlesController < ApplicationController
load_resource
def new
end
def show
# @article is already loaded
end
end
如果是 new 这个 action,效果会等于
def new
@article = Article.new
end
如果是 show 这个 action,效果会等于
def show
@article = Article.find(params[:id])
end
有好处也有坏处,好处是…你不需要自己打一行 code,坏处就是不熟 cancan 的人,找不到 @article 在哪里会惊慌失措…
load_resource
还有一些其他进阶用法,详细介绍见controller_additions.rb
。
authorize_resource
authorize_resource
就是对 resource 判断权限(根据 CanCan::Ability 里的权限表)。
而这个 resource 必定是与同名的 instance。
如果是 ArticlesController 对应的必然是 @article。
但是你会想说这样惨了?万一我在 ArticlesController 里面要用 @post 怎么办呢?
可以在 controller 里面指定 资源实例的名字: authorize_resource :post
class ArticlesController < ApplicationController
authorize_resource :post
def new
@post = Article.new
end
def show
@post = Article.find(params[:id])
end
end
Ability 代码如下:
can :read, Post
can :create, Post
can :update, Post
resource 规则小结
所以 cancan 里面的 resource 第一个会去吃 controller 的名称当成 resource name,如果是 ArticlesController,instance 就会是 @article,而在 ability 里面就会是 can :read, Article。这是在假设你已经使用同名设计 resource & controller 的情况下。
如果非同名。你可以做出指定:authorize_resource :post,虽然是 ArticlesController,但是这一组的 resource 名称为 post,所以 instance 就会是 @post,而在 ability 里面就会是 can :read, Post。
一般开发者常会误会的是
- ability 会绑到 model,实际上不是
- controller 名称要与 @instance 名称相同,实际上不一定
- @instance 要与 model 同名,实际上不用
- ability 吃的应该是 controller name,实际上不一定(吃的是 resource name,且可以被指定)。
Cancan 吃的是 resource,而且自作聪明的假设了大家「应该」都同名,而且 README example 也是使用「同名」,才会造成了这么多的误解…
如果你有更多疑问,可以直接看 controller_resource.rb
的元代码, 相信会让你对整个架构更加的清楚… 下面介绍ability的设计。
角色判断 current_ability
这是一段普通的 ability.rb 权限范例 code。
class Ability
include CanCan::Ability
def initialize(user)
if user.blank?
# not logged in
cannot :manage, :all
basic_read_only
elsif user.has_role?(:admin)
# admin
can :manage, :all
end
end
protected
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
end
一般开发者最有疑问的是def initialize(user)
这一段程序码中的 user 到底是怎么来的?怎么会没头没尾的天外飞来一个 user,然后对这个 user 进行角色判断就可以动了?
这一段要追溯到lib/controller_additions.rb
中的这一段 current_ability。
cancan 里面去判断是否有权限的一直是 current_abibilty
,而 current_abibilty initialize
的方式就是塞 current_user 进去。
def current_ability
@current_ability ||= ::Ability.new(current_user)
end
所以 initialize(user) 里的 if user.blank? 其实就等于 if current_user.blank?(若没登入)。
这样去解读程序码,看起来就好理解很多了… 权限类别解说 :manage, :all, ..etc.
cancan 里面用了一堆自定义缩写,如 :manage、:read、:update、:all,让人不是很了解在做什么。
- :manage: 是指这个 controller 内所有的 action
- :read : 指 :index 和 :show
- :update: 指 :edit 和 :update
- :destroy: 指 :destroy
- :create: 指 :new 和 :crate
而 :all 是指所有 object (resource)
当然,不只是 CRUD 的 method 才可以被列上去,如果你有其他非 RESTful 的 method 如 :search,也是可以写上去..,只是要一条一条列上去,有点麻烦就是了。 组合技:alias_action
cancan 还提供了组合技,要是嫌原先的 :update, :read 这种组合包不够用。还可以用 alias_action 自己另外再组。例如把 :update 和 :destroy 组成 :modify。
alias_action :update, :destroy, :to => :modify
can :modify, Comment
组合技: 定制method
要是你嫌每个角色都要一条一条把权限列上去,超麻烦。可以把一些共通的权限包成 method。用叠加 method 上去的方式列举。比如把基础权限都包成 basic_read_only、account_manager_only, etc…
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
针对物件状态控管
在 User story 中,使用者固然 can :update, Topic,但还是让人觉得觉得哪里有点怪怪的?
是的。使用者应该只能编辑和修改属于自己的文章,can :update, Topic 只有说使用者可以「修改文章」啊(等于可以修改所有文章) XD
所以 ability.rb 就要这样设计了
can :update, Topic do |topic|
(topic.user_id == user.id)
end
can :destroy, Topic do |topic|
(topic.user_id == user.id)
end
可以玩的更加进阶:
can :publish, Post do |post|
( post.draft? || post.submitted? ) && !post.published?
end
其他
cancan 还有其他进阶主题可以继续探讨,读者可以自行研究:
- Nested Resources
- Exception Handling
- Ensure Authorization
不过关于「难懂」和「难用」的部分,我想我应该讲的差不多了…
小结
在写这一系列文章时,我发现 cancan 的作者,其实把大部分的文件与范例,都写在 lib/ 下的 RDOC 里面了,光看 code comment 其实就可以了解大半流程。
不过我觉得 cancan 让人觉得难读的最大原因,可能还是官方缺乏一个 example ability.rb,对于被隐藏的自动完成部分也缺乏解释,所以才造成大家觉得 cancan 是个难用的 magic library。事实上如果你开始搞懂 cancan 怎么撰写的话,它是可以帮你把网站的权限 code 处理的非常漂亮又易懂的。
后记
又积累了一项能力,cancan相关的。
傲娇的使用Disqus