-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathruby-cancancan.html
More file actions
360 lines (261 loc) · 19.7 KB
/
ruby-cancancan.html
File metadata and controls
360 lines (261 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
<!DOCTYPE html>
<html>
<head>
<!-- Document Settings -->
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Page Meta -->
<title>Cancancan 的实现原理</title>
<meta name="description" content="学习的一些记录" />
<!-- Mobile Meta -->
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Brand icon -->
<link rel="shortcut icon" href="/assets/images/favicon.ico" >
<!-- Styles'n'Scripts -->
<link rel="stylesheet" type="text/css" href="/assets/css/screen.css" />
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Merriweather:300,700,700italic,300italic|Open+Sans:700,400" />
<link rel="stylesheet" type="text/css" href="/assets/css/syntax.css" />
<!-- highlight.js -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/styles/default.min.css">
<style>.hljs { background: none; }</style>
<!-- Ghost outputs important style and meta data with this tag -->
<link rel="canonical" href="http://localhost:4000//ruby-cancancan" />
<meta name="referrer" content="origin" />
<link rel="next" href="/page2/" />
<meta property="og:site_name" content="Thinking" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Cancancan 的实现原理" />
<meta property="og:description" content="学习的一些记录" />
<meta property="og:url" content="http://localhost:4000//ruby-cancancan" />
<meta property="og:image" content="/assets/images/cover3.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Cancancan 的实现原理" />
<meta name="twitter:description" content="学习的一些记录" />
<meta name="twitter:url" content="http://localhost:4000//ruby-cancancan" />
<meta name="twitter:image:src" content="/assets/images/cover3.jpg" />
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Website",
"publisher": "Thinking",
"name": "Cancancan 的实现原理",
"url": "http://localhost:4000//ruby-cancancan",
"image": "/assets/images/cover3.jpg",
"description": "学习的一些记录"
}
</script>
<meta name="generator" content="Jekyll 3.0.0" />
<link rel="alternate" type="application/rss+xml" title="Thinking" href="/feed.xml" />
</head>
<body class="home-template nav-closed">
<!-- The blog navigation links -->
<div class="nav">
<h3 class="nav-title">Menu</h3>
<a href="#" class="nav-close">
<span class="hidden">Close</span>
</a>
<ul>
<li class="nav-home " role="presentation"><a href="/">Home</a></li>
<li class="nav-about " role="presentation"><a href="/about">About</a></li>
<li class="nav-fables " role="presentation"><a href="/tag/fables">Fables</a></li>
<li class="nav-speeches " role="presentation"><a href="/tag/speeches">Speeches</a></li>
<li class="nav-fiction " role="presentation"><a href="/tag/fiction">Fiction</a></li>
<li class="nav-author " role="presentation"><a href="/author/casper">Casper</a></li>
<li class="nav-author " role="presentation"><a href="/author/edgar">Edgar</a></li>
<li class="nav-author " role="presentation"><a href="/author/abraham">Abraham</a></li>
<li class="nav-author " role="presentation"><a href="/author/martin">Martin</a></li>
<li class="nav-author " role="presentation"><a href="/author/lewis">Lewis</a></li>
</ul>
<a class="subscribe-button icon-feed" href="/feed.xml">Subscribe</a>
</div>
<span class="nav-cover"></span>
<div class="site-wrapper">
<!-- All the main content gets inserted here, index.hbs, post.hbs, etc -->
<!-- default -->
<!-- The comment above "< default" means - insert everything in this file into -->
<!-- the [body] of the default.hbs template, which contains our header/footer. -->
<!-- Everything inside the #post tags pulls data from the post -->
<!-- #post -->
<header class="main-header post-head " style="background-image: url(/assets/images/cover3.jpg) ">
<nav class="main-nav overlay clearfix">
<a class="blog-logo" href="/"><img src="/assets/images/ghost.png" alt="Blog Logo" /></a>
<a class="menu-button icon-menu" href="#"><span class="word">Menu</span></a>
</nav>
</header>
<main class="content" role="main">
<article class="post tag-test tag-content">
<header class="post-header">
<h1 class="post-title">Cancancan 的实现原理</h1>
<section class="post-meta">
<!-- <a href='/'></a> -->
<time class="post-date" datetime="2019-03-30">30 Mar 2019</time>
<!-- [[tags prefix=" on "]] -->
on
<a href='/tag/ruby'>Ruby</a>,
<a href='/tag/rails'>Rails</a>,
<a href='/tag/cancancan'>Cancancan</a>
</section>
</header>
<section class="post-content">
<p>用了几年的<a href="https://github.com/CanCanCommunity/cancancan">cancancan</a>,说起来有些尴尬。明明是也是用基本的代码写出来的,可是却不知道是怎么搭建起来的。用的时候去查看文档,每次都要使用在controller中能找到current_user方法,需要定义一个初始的Ability Model,然后定义各种各样的can方法,然后在每个需要权限的地方用can?去判断,更具有魔力的是controller方法中直接添加 <code class="highlighter-rouge">load_and_authorize_resource</code> 就可以判断权限了,不需要做其它的判断,各种各样的黑魔法。每每想起这些都觉得有点慌,使用的时候只要功能运行成功了就私自窃喜,满满的成就感,谁知道那只是大脑的一种欺骗。其实只是用对了,对于其中是怎么发生的,始终不知所以然。由于现在开发的系统对cancancan依赖的比较大,总结一些其中实现的原理。</p>
<h4 id="cancancan实现的原理">Cancancan实现的原理</h4>
<p>简单的说,主要的逻辑线就是通过在<code class="highlighter-rouge">ability.rb</code>那个文件中声明can方法的权限,在gem中会把那个方法声明的操作和资源(就是model)做一个存储,变成一个虚拟模型(下面详细分析)。然后在controller的<code class="highlighter-rouge">authorize!</code>和view的<code class="highlighter-rouge">can?</code>方法的调用时,就用那个模型去做判断就可以了。</p>
<p>首先为ActionController::Base类定义一些基本的方法,继承了这个类的controller都会定义好这些方法。其中比较常用的是<code class="highlighter-rouge">authorize!</code>, <code class="highlighter-rouge">load_and_authorize_resource</code>(这个方法在每个action执行之前执行<code class="highlighter-rouge">load_resource</code>和<code class="highlighter-rouge">authorize_resource</code>方法),同时会声明:can?, :cannot?, :current_ability方法为helper方法,这样在view中就可以使用这三个方法了。下面是对controller做方法扩展。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if defined? ActionController::Base
ActionController::Base.class_eval do
include CanCan::ControllerAdditions
end
end
</code></pre></div></div>
<p>在Gem里面定义了current_ability方法,把current_user作为参数使用,所以需要在应用中已经定义了这个方法,要不然就会报没有这个方法定义。同时如果应用中表示当前的用户如果不是current_user,而是其它如<code class="highlighter-rouge">current_manager</code>之类的,就需要在应用的controller中定义一个current_ability的方法去覆盖这个方法。很多的权限判断都是基于这个方法去调用的。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def current_ability
@current_ability ||= ::Ability.new(current_manager)
end
</code></pre></div></div>
<p>那你的应用是通过什么方式和Gem里面的那些方法发生关联的呢?其实主要的关联是上面方法中new出来的那个Ability类,这个类是一个基本的model,执行 <code class="highlighter-rouge">rails g cancan:ability</code> 的时候会自动为我们创建一个初始化的model,initialize方法中去把定义的权限转化为虚拟的模型,然后在后面需要使用时去判断使用。当在initialize方法中定义一个权限 <code class="highlighter-rouge">can :edit, Post</code>,这时会调用到 <code class="highlighter-rouge">include CanCan::Ability</code> 中的can方法,这个方法的定义是通过在model Ability中include添加进去的。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ~/.rvm/gems/ruby-2.4.3/gems/cancancan-2.0.0/lib/cancan/ability.rb
def can(action = nil, subject = nil, conditions = nil, &block)
add_rule(Rule.new(true, action, subject, conditions, block))
end
def add_rule(rule)
rules << rule
add_rule_to_index(rule, rules.size - 1)
end
</code></pre></div></div>
<p>can方法中初始化的Rule实例表示的是一个can方法调用的一个规则,所有的规则都会添加到@rules实例变量中去。其中Rule类中定义的方法<code class="highlighter-rouge">matches_conditions</code>是后面需要用来判断找到的Rule是否符合权限的关键方法。而<code class="highlighter-rouge">add_rule_to_index</code>方法是用来存储subjects(can方法中定义的资源,即model)在Rules中定义的位置数据的格式为{model: [1,2,3]}这种。这种数据结构是为了比较容易通过model去找rules中对应的rule。在Ability的initizlie中定义的can方法到这里就执行完了。其实这个过程就是通过can方法定义了一组的rules,然后把model作为key,rule在rules中的位置作为values的存储结构模型,在需要判断权限的地方判断一下就好了,仅此而已。。。。。</p>
<p>接下来就到了在view中调用can?和在controller中调用authorize!的权限判断了,其实controller中authorize!的判断也是通过调用Gem中定义好的can?方法去执行判断的,判断到为false时就执行<code class="highlighter-rouge">raise AccessDenied</code> 所以下面只分析can?方法了。</p>
<p>调用can?方法时是通过调用 <code class="highlighter-rouge">current_ability</code>(即CanCan::Ability)中的can?方法去做判断的。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ~/.rvm/gems/ruby-2.4.3/gems/cancancan-2.0.0/lib/cancan/ability.rb
def can?(action, subject, *extra_args)
match = extract_subjects(subject).lazy.map do |a_subject|
relevant_rules_for_match(action, a_subject).detect do |rule|
rule.matches_conditions?(action, a_subject, extra_args)
end
end.reject(&:nil?).first
match ? match.base_behavior : false
end
def relevant_rules(action, subject)
return [] unless @rules
relevant = possible_relevant_rules(subject).select do |rule|
rule.expanded_actions = expand_actions(rule.actions)
rule.relevant? action, subject
end
relevant.reverse!.uniq!
optimize_order! relevant
relevant
end
def possible_relevant_rules(subject)
if subject.is_a?(Hash)
rules
else
positions = @rules_index.values_at(subject, *alternative_subjects(subject))
positions.flatten!.sort!
positions.map { |i| @rules[i] }
end
end
</code></pre></div></div>
<p>上面是主要的调用栈,relevant_rules方法是为了判断找出的rule是否符合在资源上定义的action。调用栈中的<code class="highlighter-rouge">possible_relevant_rules</code>是为了找到这个subject相关的rules,因为subject不仅仅是本身,也有可能是<a href="https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html">STI</a>,所以通过<code class="highlighter-rouge">subject.ancestors</code>找出所有相关的祖父类。然后返回相应的rules。通过 <code class="highlighter-rouge">rule.matches_conditions?(action, a_subject, extra_args)</code>去判断是否满足定义的这种情况,默认的Ability的model中can方法没有condition或者块时,一般会愉快的返回true的,但是如果有condition和block时就会转去执行相应的判断了。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def matches_conditions?(action, subject, extra_args)
if @match_all # 这种情况是can直接用block作为参数,没有action和subject的情况
call_block_with_all(action, subject, extra_args)
elsif @block && !subject_class?(subject)
@block.call(subject, *extra_args)
elsif @conditions.is_a?(Hash) && subject.class == Hash
nested_subject_matches_conditions?(subject)
elsif @conditions.is_a?(Hash) && !subject_class?(subject)
matches_conditions_hash?(subject)
else
# Don't stop at "cannot" definitions when there are conditions.
conditions_empty? ? true : @base_behavior
end
end
</code></pre></div></div>
<p>上面的判断 <code class="highlighter-rouge">@block && !subject_class?(subject)</code>这里可能要注意一下,这里和下面的误用有关联。为什么需要判断subject是否是class呢?这个和要调用的块有关,由于块中需要传入对应的实例来判断是否满足情况。如果subject是类,则不会进行该判断。所以如果定义权限时,can方法接了block,这里会有两种方式做判断,方式一时<code class="highlighter-rouge">can?(:action, Model)</code>,方式二是 <code class="highlighter-rouge">can?(action, model)</code> 一个model是类,一个是实例。如果是类,则不会执行后面的块,只有资源是实例时才会执行block的判断。</p>
<h4 id="controller中的-load_and_authorize_resource-方法做了什么">Controller中的 <code class="highlighter-rouge">load_and_authorize_resource</code> 方法做了什么</h4>
<p>调用这个方法时,其实是在添加了一个<code class="highlighter-rouge">before_action</code>的声明。在这里就相当于声明了</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>before_action :load_and_authorize_resource
</code></pre></div></div>
<p>而load_and_authorize_resource方法分别调用<code class="highlighter-rouge">load_resource</code>和<code class="highlighter-rouge">authorize_resource</code>方法。和在controller中分别声明那两个方法意思一样。</p>
<p>load_resource方法是通过controller的名字找到对应的model名,把model名设置为实例变量,这个实例变量供后面<code class="highlighter-rouge">authorize_resource</code>调用时使用。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ~/.rvm/gems/ruby-2.4.3/gems/cancancan-2.0.0/lib/cancan/controller_resource.rb
def load_resource
return if skip?(:load)
if load_instance? # 根据情况决定是否把资源load出来
self.resource_instance ||= load_resource_instance
elsif load_collection?
self.collection_instance ||= load_collection
end
end
def load_instance?
# parent的意思是由于有些资源是嵌套的,比如/users/:user_id/products/3这种形式,parent就是user了,则需要用
# load_resource :user
# load_resource :product, through: :user
parent? ||
# 是否是member_action,则根据是否是new,create,或者params中有id的那种情况(update, destroy)等其它情况
member_action?
end
def load_collection # accessible_by方法会根据can方法中设置的condition用where自动作为condition去查询
resource_base.accessible_by(current_ability, authorization_action)
end
</code></pre></div></div>
<p>这样上面就是对load_resource的理解了,其实简单点说就是先帮你把各种资源假设查找出来,然后在<code class="highlighter-rouge">authorize_resource</code>的时候用这个去判断权限。不过有时候我们用权限的方式不太对,但是没有检查到。比如说设置了如下的权限:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>can :edit, User do |resource|
resource.normal?
end
</code></pre></div></div>
<p>然后在view里用的却用<code class="highlighter-rouge">can?(:edit, User)</code>这种验证方法,这时候其实块里面的权限没有检查到。
还有一种情况就是controller里加载的resource和model没有对应,然后直接用 <code class="highlighter-rouge">load_and_authorize_resource</code>方法去验证权限了,这样也是不对的。这时候就需要添加一些name和class的参数去把这些和model名字对应起来了。</p>
<h4 id="总结">总结</h4>
<p>总的来说,权限其实是实例化了一个Ability,然后在其中保存rules权限匹配规则。在需要判断的时候用can调用,利用参数action和其中的资源去匹配,看其中有没有对应的规则可以匹配去做权限验证。看文档能解决使用问题,但是要精确的去使用还是要看下里面的实现。</p>
</section>
</article>
</main>
<aside class="read-next">
<!-- [[! next_post ]] -->
<a class="read-next-story " style="background-image: url(/assets/images/cover3.jpg)" href="/ruby-warden">
<section class="post">
<h2>Ruby Warden</h2>
<p></p>
</section>
</a>
<!-- [[! /next_post ]] -->
<!-- [[! prev_post ]] -->
<a class="read-next-story prev " style="background-image: url(/assets/images/cover3.jpg)" href="/ruby-sprocket">
<section class="post">
<h2>Srpocket 如何加入gem静态文件路径</h2>
<p></p>
</section>
</a>
<!-- [[! /prev_post ]] -->
</aside>
<!-- /post -->
<!-- The tiny footer at the very bottom -->
<footer class="site-footer clearfix">
<section class="copyright"><a href="/">Thinking</a> © 2020</section>
<section class="poweredby">Proudly published with <a href="https://jekyllrb.com/">Jekyll</a> using <a href="https://github.com/jekyller/jasper">Jasper</a></section>
</footer>
</div>
<!-- highlight.js -->
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- jQuery needs to come before `` so that jQuery can be used in code injection -->
<script type="text/javascript" src="//code.jquery.com/jquery-1.12.0.min.js"></script>
<!-- Ghost outputs important scripts and data with this tag -->
<!-- -->
<!-- Add Google Analytics -->
<!-- Google Analytics Tracking code -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-69281367-1', 'auto');
ga('send', 'pageview');
</script>
<!-- Fitvids makes video embeds responsive and awesome -->
<script type="text/javascript" src="/assets/js/jquery.fitvids.js"></script>
<!-- The main JavaScript file for Casper -->
<script type="text/javascript" src="/assets/js/index.js"></script>
</body>
</html>