-
Notifications
You must be signed in to change notification settings - Fork 2
/
action_controller_overview.html
1188 lines (1066 loc) · 75.1 KB
/
action_controller_overview.html
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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN" lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Action Controller 简介 — Ruby on Rails 指南</title>
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print" />
<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shThemeRailsGuides.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/fixes.css" />
<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
</head>
<body class="guide">
<div id="topNav">
<div class="wrapper">
<strong class="more-info-label">更多内容 <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong>
<span class="red-button more-info-button">
更多内容
</span>
<ul class="more-info-links s-hidden">
<li class="more-info"><a href="http://rubyonrails.org/">综览</a></li>
<li class="more-info"><a href="http://rubyonrails.org/download">下载</a></li>
<li class="more-info"><a href="http://rubyonrails.org/deploy">部署</a></li>
<li class="more-info"><a href="https://github.com/rails/rails">源码</a></li>
<li class="more-info"><a href="http://rubyonrails.org/screencasts">视频</a></li>
<li class="more-info"><a href="http://rubyonrails.org/documentation">文件</a></li>
<li class="more-info"><a href="http://rubyonrails.org/community">社群</a></li>
<li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
</ul>
</div>
</div>
<div id="header">
<div class="wrapper clearfix">
<h1><a href="index.html" title="回首页">Guides.rubyonrails.org</a></h1>
<ul class="nav">
<li><a class="nav-item" href="index.html">首页</a></li>
<li class="guides-index guides-index-large">
<a href="index.html" id="guidesMenu" class="guides-index-item nav-item">指南目录</a>
<div id="guides" class="clearfix" style="display: none;">
<hr />
<dl class="L">
<dt>入门</dt>
<dd><a href="getting_started.html">Rails 入门</a></dd>
<dt>模型</dt>
<dd><a href="active_record_basics.html">Active Record 基础</a></dd>
<dd><a href="active_record_migrations.html">Active Record 数据库迁移</a></dd>
<dd><a href="active_record_validations.html">Active Record 数据验证</a></dd>
<dd><a href="active_record_callbacks.html">Active Record 回调</a></dd>
<dd><a href="association_basics.html">Active Record 关联</a></dd>
<dd><a href="active_record_querying.html">Active Record 查询</a></dd>
<dt>视图</dt>
<dd><a href="layouts_and_rendering.html">Rails 布局和视图渲染</a></dd>
<dd><a href="form_helpers.html">Action View 表单帮助方法</a></dd>
<dt>控制器</dt>
<dd><a href="action_controller_overview.html">Action Controller 简介</a></dd>
<dd><a href="routing.html">Rails 路由全解</a></dd>
</dl>
<dl class="R">
<dt>深入</dt>
<dd><a href="active_support_core_extensions.html">Active Support 核心扩展</a></dd>
<dd><a href="i18n.html">Rails 国际化 API</a></dd>
<dd><a href="action_mailer_basics.html">Action Mailer 基础</a></dd>
<dd><a href="active_job_basics.html">Active Job 基础</a></dd>
<dd><a href="security.html">Rails 安全指南</a></dd>
<dd><a href="debugging_rails_applications.html">调试 Rails 程序</a></dd>
<dd><a href="configuring.html">设置 Rails 程序</a></dd>
<dd><a href="command_line.html">Rails 命令行</a></dd>
<dd><a href="asset_pipeline.html">Asset Pipeline</a></dd>
<dd><a href="working_with_javascript_in_rails.html">在 Rails 中使用 JavaScript</a></dd>
<dd><a href="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</a></dd>
<dt>扩展 Rails</dt>
<dd><a href="rails_on_rack.html">Rails on Rack</a></dd>
<dd><a href="generators.html">客制与新建 Rails 产生器</a></dd>
<dd><a href="rails_application_templates.html">Rails 应用程式模版</a></dd>
<dt>贡献 Ruby on Rails</dt>
<dd><a href="contributing_to_ruby_on_rails.html">贡献 Ruby on Rails</a></dd>
<dd><a href="api_documentation_guidelines.html">API 文件准则</a></dd>
<dd><a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南准则</a></dd>
<dt>维护方针</dt>
<dd><a href="maintenance_policy.html">维护方针</a></dd>
<dt>发布记</dt>
<dd><a href="upgrading_ruby_on_rails.html">升级 Ruby on Rails</a></dd>
<dd><a href="4_2_release_notes.html">Ruby on Rails 4.2 发布记</a></dd>
<dd><a href="4_1_release_notes.html">Ruby on Rails 4.1 发布记</a></dd>
<dd><a href="4_0_release_notes.html">Ruby on Rails 4.0 发布记</a></dd>
<dd><a href="3_2_release_notes.html">Ruby on Rails 3.2 发布记</a></dd>
<dd><a href="3_1_release_notes.html">Ruby on Rails 3.1 发布记</a></dd>
<dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 发布记</a></dd>
<dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 发布记</a></dd>
<dd><a href="2_2_release_notes.html">Ruby on Rails 2.2 发布记</a></dd>
</dl>
</div>
</li>
<!-- <li><a class="nav-item" href="//github.com/docrails-tw/wiki">参与翻译</a></li> -->
<li><a class="nav-item" href="https://github.com/ruby-china/guides/blob/master/CONTRIBUTING.md">贡献</a></li>
<li><a class="nav-item" href="credits.html">致谢</a></li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">指南目录</option>
<optgroup label="入门">
<option value="getting_started.html">Rails 入门</option>
</optgroup>
<optgroup label="模型">
<option value="active_record_basics.html">Active Record 基础</option>
<option value="active_record_migrations.html">Active Record 数据库迁移</option>
<option value="active_record_validations.html">Active Record 数据验证</option>
<option value="active_record_callbacks.html">Active Record 回调</option>
<option value="association_basics.html">Active Record 关联</option>
<option value="active_record_querying.html">Active Record 查询</option>
</optgroup>
<optgroup label="视图">
<option value="layouts_and_rendering.html">Rails 布局和视图渲染</option>
<option value="form_helpers.html">Action View 表单帮助方法</option>
</optgroup>
<optgroup label="控制器">
<option value="action_controller_overview.html">Action Controller 简介</option>
<option value="routing.html">Rails 路由全解</option>
</optgroup>
<optgroup label="深入">
<option value="active_support_core_extensions.html">Active Support 核心扩展</option>
<option value="i18n.html">Rails 国际化 API</option>
<option value="action_mailer_basics.html">Action Mailer 基础</option>
<option value="active_job_basics.html">Active Job 基础</option>
<option value="security.html">Rails 安全指南</option>
<option value="debugging_rails_applications.html">调试 Rails 程序</option>
<option value="configuring.html">设置 Rails 程序</option>
<option value="command_line.html">Rails 命令行</option>
<option value="asset_pipeline.html">Asset Pipeline</option>
<option value="working_with_javascript_in_rails.html">在 Rails 中使用 JavaScript</option>
<option value="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</option>
</optgroup>
<optgroup label="扩展 Rails">
<option value="rails_on_rack.html">Rails on Rack</option>
<option value="generators.html">客制与新建 Rails 产生器</option>
<option value="rails_application_templates.html">Rails 应用程式模版</option>
</optgroup>
<optgroup label="贡献 Ruby on Rails">
<option value="contributing_to_ruby_on_rails.html">贡献 Ruby on Rails</option>
<option value="api_documentation_guidelines.html">API 文件准则</option>
<option value="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南准则</option>
</optgroup>
<optgroup label="维护方针">
<option value="maintenance_policy.html">维护方针</option>
</optgroup>
<optgroup label="发布记">
<option value="upgrading_ruby_on_rails.html">升级 Ruby on Rails</option>
<option value="4_2_release_notes.html">Ruby on Rails 4.2 发布记</option>
<option value="4_1_release_notes.html">Ruby on Rails 4.1 发布记</option>
<option value="4_0_release_notes.html">Ruby on Rails 4.0 发布记</option>
<option value="3_2_release_notes.html">Ruby on Rails 3.2 发布记</option>
<option value="3_1_release_notes.html">Ruby on Rails 3.1 发布记</option>
<option value="3_0_release_notes.html">Ruby on Rails 3.0 发布记</option>
<option value="2_3_release_notes.html">Ruby on Rails 2.3 发布记</option>
<option value="2_2_release_notes.html">Ruby on Rails 2.2 发布记</option>
</optgroup>
</select>
</li>
</ul>
</div>
</div>
</div>
<hr class="hide" />
<div id="feature">
<div class="wrapper">
<h2>Action Controller 简介</h2><p>本文介绍控制器的工作原理,以及控制器在程序请求周期内扮演的角色。</p><p>读完本文,你将学到:</p>
<ul>
<li>请求如何进入控制器;</li>
<li>如何限制传入控制器的参数;</li>
<li>为什么以及如何把数据存储在会话或 cookie 中;</li>
<li>处理请求时,如何使用过滤器执行代码;</li>
<li>如何使用 Action Controller 內建的 HTTP 身份认证功能;</li>
<li>如何把数据流直发送给用户的浏览器;</li>
<li>如何过滤敏感信息,不写入程序的日志;</li>
<li>如何处理请求过程中可能出现的异常;</li>
</ul>
<div id="subCol">
<h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />Chapters</h3>
<ol class="chapters">
<li><a href="#%E6%8E%A7%E5%88%B6%E5%99%A8%E7%9A%84%E4%BD%9C%E7%94%A8">控制器的作用</a></li>
<li><a href="#%E6%8E%A7%E5%88%B6%E5%99%A8%E5%91%BD%E5%90%8D%E7%BA%A6%E5%AE%9A">控制器命名约定</a></li>
<li><a href="#%E6%96%B9%E6%B3%95%E5%92%8C%E5%8A%A8%E4%BD%9C">方法和动作</a></li>
<li>
<a href="#%E5%8F%82%E6%95%B0">参数</a>
<ul>
<li><a href="#hash-%E5%92%8C%E6%95%B0%E7%BB%84%E5%8F%82%E6%95%B0">Hash 和数组参数</a></li>
<li><a href="#json-%E5%8F%82%E6%95%B0">JSON 参数</a></li>
<li><a href="#%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0">路由参数</a></li>
<li><a href="#default_url_options"><code>default_url_options</code></a></li>
<li><a href="#%E5%81%A5%E5%A3%AE%E5%8F%82%E6%95%B0">健壮参数</a></li>
</ul>
</li>
<li>
<a href="#%E4%BC%9A%E8%AF%9D">会话</a>
<ul>
<li><a href="#%E8%8E%B7%E5%8F%96%E4%BC%9A%E8%AF%9D">获取会话</a></li>
<li><a href="#flash-%E6%B6%88%E6%81%AF">Flash 消息</a></li>
</ul>
</li>
<li><a href="#cookies">Cookies</a></li>
<li><a href="#%E6%B8%B2%E6%9F%93-xml-%E5%92%8C-json-%E6%95%B0%E6%8D%AE">渲染 XML 和 JSON 数据</a></li>
<li>
<a href="#%E8%BF%87%E6%BB%A4%E5%99%A8">过滤器</a>
<ul>
<li><a href="#%E5%90%8E%E7%BD%AE%E8%BF%87%E6%BB%A4%E5%99%A8%E5%92%8C%E7%8E%AF%E7%BB%95%E8%BF%87%E6%BB%A4%E5%99%A8">后置过滤器和环绕过滤器</a></li>
<li><a href="#%E8%BF%87%E6%BB%A4%E5%99%A8%E7%9A%84%E5%85%B6%E4%BB%96%E7%94%A8%E6%B3%95">过滤器的其他用法</a></li>
</ul>
</li>
<li><a href="#%E9%98%B2%E6%AD%A2%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0">防止请求伪造</a></li>
<li>
<a href="#request-%E5%92%8C-response-%E5%AF%B9%E8%B1%A1"><code>request</code> 和 <code>response</code> 对象</a>
<ul>
<li><a href="#request-%E5%AF%B9%E8%B1%A1"><code>request</code> 对象</a></li>
<li><a href="#response-%E5%AF%B9%E8%B1%A1"><code>response</code> 对象</a></li>
</ul>
</li>
<li>
<a href="#http-%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81">HTTP 身份认证</a>
<ul>
<li><a href="#http-%E5%9F%BA%E6%9C%AC%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81">HTTP 基本身份认证</a></li>
<li><a href="#http-%E6%91%98%E8%A6%81%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81">HTTP 摘要身份认证</a></li>
</ul>
</li>
<li>
<a href="#%E6%95%B0%E6%8D%AE%E6%B5%81%E5%92%8C%E6%96%87%E4%BB%B6%E4%B8%8B%E8%BD%BD">数据流和文件下载</a>
<ul>
<li><a href="#%E5%8F%91%E9%80%81%E6%96%87%E4%BB%B6">发送文件</a></li>
<li><a href="#%E4%BD%BF%E7%94%A8-rest-%E7%9A%84%E6%96%B9%E5%BC%8F%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6">使用 REST 的方式下载文件</a></li>
<li><a href="#%E4%BB%BB%E6%84%8F%E6%95%B0%E6%8D%AE%E7%9A%84%E5%AE%9E%E6%97%B6%E6%B5%81">任意数据的实时流</a></li>
</ul>
</li>
<li>
<a href="#%E8%BF%87%E6%BB%A4%E6%97%A5%E5%BF%97">过滤日志</a>
<ul>
<li><a href="#%E8%BF%87%E6%BB%A4%E5%8F%82%E6%95%B0">过滤参数</a></li>
<li><a href="#%E8%BF%87%E6%BB%A4%E8%BD%AC%E5%90%91">过滤转向</a></li>
</ul>
</li>
<li>
<a href="#%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86">异常处理</a>
<ul>
<li><a href="#%E9%BB%98%E8%AE%A4%E7%9A%84-500-%E5%92%8C-404-%E6%A8%A1%E6%9D%BF">默认的 500 和 404 模板</a></li>
<li><a href="#rescue_from"><code>rescue_from</code></a></li>
</ul>
</li>
<li><a href="#%E5%BC%BA%E5%88%B6%E4%BD%BF%E7%94%A8-https-%E5%8D%8F%E8%AE%AE">强制使用 HTTPS 协议</a></li>
</ol>
</div>
</div>
</div>
<div id="container">
<div class="wrapper">
<div id="mainCol">
<h3 id="控制器的作用">1 控制器的作用</h3><p>Action Controller 是 MVC 中的 C(控制器)。路由决定使用哪个控制器处理请求后,控制器负责解析请求,生成对应的请求。Action Controller 会代为处理大多数底层工作,使用易懂的约定,让整个过程清晰明了。</p><p>在大多数按照 <a href="http://en.wikipedia.org/wiki/Representational_state_transfer">REST</a> 规范开发的程序中,控制器会接收请求(开发者不可见),从模型中获取数据,或把数据写入模型,再通过视图生成 HTML。如果控制器需要做其他操作,也没问题,以上只不过是控制器的主要作用。</p><p>因此,控制器可以视作模型和视图的中间人,让模型中的数据可以在视图中使用,把数据显示给用户,再把用户提交的数据保存或更新到模型中。</p><div class="note"><p>路由的处理细节请查阅 <a href="routing.html">Rails Routing From the Outside In</a>。</p></div><h3 id="控制器命名约定">2 控制器命名约定</h3><p>Rails 控制器的命名习惯是,最后一个单词使用<strong>复数形式</strong>,但也是有例外,比如 <code>ApplicationController</code>。例如:用 <code>ClientsController</code>,而不是 <code>ClientController</code>;用 <code>SiteAdminsController</code>,而不是 <code>SiteAdminController</code> 或 <code>SitesAdminsController</code>。</p><p>遵守这一约定便可享用默认的路由生成器(例如 <code>resources</code> 等),无需再指定 <code>:path</code> 或 <code>:controller</code>,URL 和路径的帮助方法也能保持一致性。详情参阅 <a href="layouts_and_rendering.html">Layouts & Rendering Guide</a>。</p><div class="note"><p>控制器的命名习惯和模型不同,模型的名字习惯使用单数形式。</p></div><h3 id="方法和动作">3 方法和动作</h3><p>控制器是一个类,继承自 <code>ApplicationController</code>,和其他类一样,定义了很多方法。程序接到请求时,路由决定运行哪个控制器和哪个动作,然后创建该控制器的实例,运行和动作同名的方法。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
def new
end
end
</pre>
</div>
<p>例如,用户访问 <code>/clients/new</code> 新建客户,Rails 会创建一个 <code>ClientsController</code> 实例,运行 <code>new</code> 方法。注意,在上面这段代码中,即使 <code>new</code> 方法是空的也没关系,因为默认会渲染 <code>new.html.erb</code> 视图,除非指定执行其他操作。在 <code>new</code> 方法中,声明可在视图中使用的 <code>@client</code> 实例变量,创建一个新的 <code>Client</code> 实例:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def new
@client = Client.new
end
</pre>
</div>
<p>详情参阅 <a href="layouts_and_rendering.html">Layouts & Rendering Guide</a>。</p><p><code>ApplicationController</code> 继承自 <code>ActionController::Base</code>。<code>ActionController::Base</code> 定义了很多实用方法。本文会介绍部分方法,如果想知道定义了哪些方法,可查阅 API 文档或源码。</p><p>只有公开方法才被视为动作。所以最好减少对外可见的方法数量,例如辅助方法和过滤器方法。</p><h3 id="参数">4 参数</h3><p>在控制器的动作中,往往需要获取用户发送的数据,或其他参数。在网页程序中参数分为两类。第一类随 URL 发送,叫做“请求参数”,即 URL 中 <code>?</code> 符号后面的部分。第二类经常成为“POST 数据”,一般来自用户填写的表单。之所以叫做“POST 数据”是因为,只能随 HTTP POST 请求发送。Rails 不区分这两种参数,在控制器中都可通过 <code>params</code> Hash 获取:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
# This action uses query string parameters because it gets run
# by an HTTP GET request, but this does not make any difference
# to the way in which the parameters are accessed. The URL for
# this action would look like this in order to list activated
# clients: /clients?status=activated
def index
if params[:status] == "activated"
@clients = Client.activated
else
@clients = Client.inactivated
end
end
# This action uses POST parameters. They are most likely coming
# from an HTML form which the user has submitted. The URL for
# this RESTful request will be "/clients", and the data will be
# sent as part of the request body.
def create
@client = Client.new(params[:client])
if @client.save
redirect_to @client
else
# This line overrides the default rendering behavior, which
# would have been to render the "create" view.
render "new"
end
end
end
</pre>
</div>
<h4 id="hash-和数组参数">4.1 Hash 和数组参数</h4><p><code>params</code> Hash 不局限于只能使用一维键值对,其中可以包含数组和嵌套的 Hash。要发送数组,需要在键名后加上一对空方括号(<code>[]</code>):</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
GET /clients?ids[]=1&ids[]=2&ids[]=3
</pre>
</div>
<div class="note"><p>“[”和“]”这两个符号不允许出现在 URL 中,所以上面的地址会被编码成 <code>/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3</code>。大多数情况下,无需你费心,浏览器会为你代劳编码,接收到这样的请求后,Rails 也会自动解码。如果你要手动向服务器发送这样的请求,就要留点心了。</p></div><p>此时,<code>params[:ids]</code> 的值是 <code>["1", "2", "3"]</code>。注意,参数的值始终是字符串,Rails 不会尝试转换类型。</p><div class="note"><p>默认情况下,基于安全考虑,参数中的 <code>[]</code>、<code>[nil]</code> 和 <code>[nil, nil, ...]</code> 会替换成 <code>nil</code>。详情参阅<a href="security.html#unsafe-query-generation">安全指南</a>。</p></div><p>要发送嵌套的 Hash 参数,需要在方括号内指定键名:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/clients" method="post">
<input type="text" name="client[name]" value="Acme" />
<input type="text" name="client[phone]" value="12345" />
<input type="text" name="client[address][postcode]" value="12345" />
<input type="text" name="client[address][city]" value="Carrot City" />
</form>
</pre>
</div>
<p>提交这个表单后,<code>params[:client]</code> 的值是 <code>{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }</code>。注意 <code>params[:client][:address]</code> 是个嵌套 Hash。</p><p>注意,<code>params</code> Hash 其实是 <code>ActiveSupport::HashWithIndifferentAccess</code> 的实例,虽和普通的 Hash 一样,但键名使用 Symbol 和字符串的效果一样。</p><h4 id="json-参数">4.2 JSON 参数</h4><p>开发网页服务程序时,你会发现,接收 JSON 格式的参数更容易处理。如果请求的 <code>Content-Type</code> 报头是 <code>application/json</code>,Rails 会自动将其转换成 <code>params</code> Hash,按照常规的方法使用:</p><p>例如,如果发送如下的 JSON 格式内容:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
{ "company": { "name": "acme", "address": "123 Carrot Street" } }
</pre>
</div>
<p>得到的是 <code>params[:company]</code> 就是 <code>{ "name" => "acme", "address" => "123 Carrot Street" }</code>。</p><p>如果在初始化脚本中开启了 <code>config.wrap_parameters</code> 选项,或者在控制器中调用了 <code>wrap_parameters</code> 方法,可以放心的省去 JSON 格式参数中的根键。Rails 会以控制器名新建一个键,复制参数,将其存入这个键名下。因此,上面的参数可以写成:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
{ "name": "acme", "address": "123 Carrot Street" }
</pre>
</div>
<p>假设数据传送给 <code>CompaniesController</code>,那么参数会存入 <code>:company</code> 键名下:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
</pre>
</div>
<p>如果想修改默认使用的键名,或者把其他参数存入其中,请参阅 <a href="http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html">API 文档</a>。</p><div class="note"><p>解析 XML 格式参数的功能现已抽出,制成了 gem,名为 <code>actionpack-xml_parser</code>。</p></div><h4 id="路由参数">4.3 路由参数</h4><p><code>params</code> Hash 总有 <code>:controller</code> 和 <code>:action</code> 两个键,但获取这两个值应该使用 <code>controller_name</code> 和 <code>action_name</code> 方法。路由中定义的参数,例如 <code>:id</code>,也可通过 <code>params</code> Hash 获取。例如,假设有个客户列表,可以列出激活和禁用的客户。我们可以定义一个路由,捕获下面这个 URL 中的 <code>:status</code> 参数:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
get '/clients/:status' => 'clients#index', foo: 'bar'
</pre>
</div>
<p>在这个例子中,用户访问 <code>/clients/active</code> 时,<code>params[:status]</code> 的值是 <code>"active"</code>。同时,<code>params[:foo]</code> 的值也会被设为 <code>"bar"</code>,就像通过请求参数传入的一样。<code>params[:action]</code> 也是一样,其值为 <code>"index"</code>。</p><h4 id="default_url_options">4.4 <code>default_url_options</code>
</h4><p>在控制器中定义名为 <code>default_url_options</code> 的方法,可以设置所生成 URL 中都包含的参数。这个方法必须返回一个 Hash,其值为所需的参数值,而且键必须使用 Symbol:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
def default_url_options
{ locale: I18n.locale }
end
end
</pre>
</div>
<p>这个方法定义的只是预设参数,可以被 <code>url_for</code> 方法的参数覆盖。</p><p>如果像上面的代码一样,在 <code>ApplicationController</code> 中定义 <code>default_url_options</code>,则会用于所有生成的 URL。<code>default_url_options</code> 也可以在具体的控制器中定义,只影响和该控制器有关的 URL。</p><h4 id="健壮参数">4.5 健壮参数</h4><p>加入健壮参数功能后,Action Controller 的参数禁止在 Avtive Model 中批量赋值,除非参数在白名单中。也就是说,你要明确选择那些属性可以批量更新,避免意外把不该暴露的属性暴露了。</p><p>而且,还可以标记哪些参数是必须传入的,如果没有收到,会交由 <code>raise/rescue</code> 处理,返回“400 Bad Request”。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class PeopleController < ActionController::Base
# This will raise an ActiveModel::ForbiddenAttributes exception
# because it's using mass assignment without an explicit permit
# step.
def create
Person.create(params[:person])
end
# This will pass with flying colors as long as there's a person key
# in the parameters, otherwise it'll raise a
# ActionController::ParameterMissing exception, which will get
# caught by ActionController::Base and turned into that 400 Bad
# Request reply.
def update
person = current_account.people.find(params[:id])
person.update!(person_params)
redirect_to person
end
private
# Using a private method to encapsulate the permissible parameters
# is just a good pattern since you'll be able to reuse the same
# permit list between create and update. Also, you can specialize
# this method with per-user checking of permissible attributes.
def person_params
params.require(:person).permit(:name, :age)
end
end
</pre>
</div>
<h5 id="允许使用的标量值">4.5.1 允许使用的标量值</h5><p>假如允许传入 <code>:id</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.permit(:id)
</pre>
</div>
<p>若 <code>params</code> 中有 <code>:id</code>,且 <code>:id</code> 是标量值,就可以通过白名单检查,否则 <code>:id</code> 会被过滤掉。因此不能传入数组、Hash 或其他对象。</p><p>允许使用的标量类型有:<code>String</code>、<code>Symbol</code>、<code>NilClass</code>、<code>Numeric</code>、<code>TrueClass</code>、<code>FalseClass</code>、<code>Date</code>、<code>Time</code>、<code>DateTime</code>、<code>StringIO</code>、<code>IO</code>、<code>ActionDispatch::Http::UploadedFile</code> 和 <code>Rack::Test::UploadedFile</code>。</p><p>要想指定 <code>params</code> 中的值必须为数组,可以把键对应的值设为空数组:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.permit(id: [])
</pre>
</div>
<p>要想允许传入整个参数 Hash,可以使用 <code>permit!</code> 方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.require(:log_entry).permit!
</pre>
</div>
<p>此时,允许传入整个 <code>:log_entry</code> Hash 及嵌套 Hash。使用 <code>permit!</code> 时要特别注意,因为这么做模型中所有当前属性及后续添加的属性都允许进行批量赋值。</p><h5 id="嵌套参数">4.5.2 嵌套参数</h5><p>也可以允许传入嵌套参数,例如:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
params.permit(:name, { emails: [] },
friends: [ :name,
{ family: [ :name ], hobbies: [] }])
</pre>
</div>
<p>此时,允许传入 <code>name</code>,<code>emails</code> 和 <code>friends</code> 属性。其中,<code>emails</code> 必须是数组;<code>friends</code> 必须是一个由资源组成的数组:应该有个 <code>name</code> 属性,还要有 <code>hobbies</code> 属性,其值是由标量组成的数组,以及一个 <code>family</code> 属性,其值只能包含 <code>name</code> 属性(任何允许使用的标量值)。</p><h5 id="更多例子">4.5.3 更多例子</h5><p>你可能还想在 <code>new</code> 动作中限制允许传入的属性。不过此时无法再根键上调用 <code>require</code> 方法,因为此时根键还不存在:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# using `fetch` you can supply a default and use
# the Strong Parameters API from there.
params.fetch(:blog, {}).permit(:title, :author)
</pre>
</div>
<p>使用 <code>accepts_nested_attributes_for</code> 方法可以更新或销毁响应的记录。这个方法基于 <code>id</code> 和 <code>_destroy</code> 参数:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# permit :id and :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
</pre>
</div>
<p>如果 Hash 的键是数字,处理方式有所不同,此时可以把属性作为 Hash 的直接子 Hash。<code>accepts_nested_attributes_for</code> 和 <code>has_many</code> 关联同时使用时会得到这种参数:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# To whitelist the following data:
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
params.require(:book).permit(:title, chapters_attributes: [:title])
</pre>
</div>
<h5 id="不用健壮参数">4.5.4 不用健壮参数</h5><p>健壮参数的目的是为了解决常见问题,不是万用良药。不过,可以很方便的和自己的代码结合,解决复杂需求。</p><p>假设有个参数包含产品的名字和一个由任意数据组成的产品附加信息 Hash,希望过滤产品名和整个附加数据 Hash。健壮参数不能过滤由任意键值组成的嵌套 Hash,不过可以使用嵌套 Hash 的键定义过滤规则:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def product_params
params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end
</pre>
</div>
<h3 id="会话">5 会话</h3><p>程序中的每个用户都有一个会话(session),可以存储少量数据,在多次请求中永久存储。会话只能在控制器和视图中使用,可以通过以下几种存储机制实现:</p>
<ul>
<li>
<code>ActionDispatch::Session::CookieStore</code>:所有数据都存储在客户端</li>
<li>
<code>ActionDispatch::Session::CacheStore</code>:数据存储在 Rails 缓存里</li>
<li>
<code>ActionDispatch::Session::ActiveRecordStore</code>:使用 Active Record 把数据存储在数据库中(需要使用 <code>activerecord-session_store</code> gem)</li>
<li>
<code>ActionDispatch::Session::MemCacheStore</code>:数据存储在 Memcached 集群中(这是以前的实现方式,现在请改用 CacheStore)</li>
</ul>
<p>所有存储机制都会用到一个 cookie,存储每个会话的 ID(必须使用 cookie,因为 Rails 不允许在 URL 中传递会话 ID,这么做不安全)。</p><p>大多数存储机制都会使用这个 ID 在服务商查询会话数据,例如在数据库中查询。不过有个例外,即默认也是推荐使用的存储方式 CookieStore。CookieStore 把所有会话数据都存储在 cookie 中(如果需要,还是可以使用 ID)。CookieStore 的优点是轻量,而且在新程序中使用会话也不用额外的设置。cookie 中存储的数据会使用密令签名,以防篡改。cookie 会被加密,任何有权访问的人都无法读取其内容。(如果修改了 cookie,Rails 会拒绝使用。)</p><p>CookieStore 可以存储大约 4KB 数据,比其他几种存储机制都少很多,但一般也足够用了。不过使用哪种存储机制,都不建议在会话中存储大量数据。应该特别避免在会话中存储复杂的对象(Ruby 基本对象之外的一切对象,最常见的是模型实例),服务器可能无法在多次请求中重组数据,最终导致错误。</p><p>如果会话中没有存储重要的数据,或者不需要持久存储(例如使用 Falsh 存储消息),可以考虑使用 <code>ActionDispatch::Session::CacheStore</code>。这种存储机制使用程序所配置的缓存方式。CacheStore 的优点是,可以直接使用现有的缓存方式存储会话,不用额外的设置。不过缺点也很明显,会话存在时间很多,随时可能消失。</p><p>关于会话存储的更多内容请参阅<a href="security.html">安全指南</a></p><p>如果想使用其他的会话存储机制,可以在 <code>config/initializers/session_store.rb</code> 文件中设置:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "rails g active_record:session_migration")
# YourApp::Application.config.session_store :active_record_store
</pre>
</div>
<p>签署会话数据时,Rails 会用到会话的键(cookie 的名字),这个值可以在 <code>config/initializers/session_store.rb</code> 中修改:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# Be sure to restart your server when you modify this file.
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
</pre>
</div>
<p>还可以传入 <code>:domain</code> 键,指定可使用此 cookie 的域名:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# Be sure to restart your server when you modify this file.
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
</pre>
</div>
<p>Rails 为 CookieStore 提供了一个密令,用来签署会话数据。这个密令可以在 <code>config/secrets.yml</code> 文件中修改:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# Be sure to restart your server when you modify this file.
# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
# You can use `rake secret` to generate a secure secret key.
# Make sure the secrets in this file are kept private
# if you're sharing your code publicly.
development:
secret_key_base: a75d...
test:
secret_key_base: 492f...
# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
</pre>
</div>
<div class="note"><p>使用 CookieStore 时,如果修改了密令,之前所有的会话都会失效。</p></div><h4 id="获取会话">5.1 获取会话</h4><p>在控制器中,可以使用实例方法 <code>session</code> 获取会话。</p><div class="note"><p>会话是惰性加载的,如果不在动作中获取,不会自动加载。因此无需禁用会话,不获取即可。</p></div><p>会话中的数据以键值对的形式存储,类似 Hash:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
private
# Finds the User with the ID stored in the session with the key
# :current_user_id This is a common way to handle user login in
# a Rails application; logging in sets the session value and
# logging out removes it.
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
end
</pre>
</div>
<p>要想把数据存入会话,像 Hash 一样,给键赋值即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
# "Create" a login, aka "log the user in"
def create
if user = User.authenticate(params[:username], params[:password])
# Save the user ID in the session so it can be used in
# subsequent requests
session[:current_user_id] = user.id
redirect_to root_url
end
end
end
</pre>
</div>
<p>要从会话中删除数据,把键的值设为 <code>nil</code> 即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
# "Delete" a login, aka "log the user out"
def destroy
# Remove the user id from the session
@_current_user = session[:current_user_id] = nil
redirect_to root_url
end
end
</pre>
</div>
<p>要重设整个会话,请使用 <code>reset_session</code> 方法。</p><h4 id="flash-消息">5.2 Flash 消息</h4><p>Flash 是会话的一个特殊部分,每次请求都会清空。也就是说,其中存储的数据只能在下次请求时使用,可用来传递错误消息等。</p><p>Flash 消息的获取方式和会话差不多,类似 Hash。Flash 消息是 <a href="http://api.rubyonrails.org/classes/ActionDispatch/Flash/FlashHash.html">FlashHash</a> 实例。</p><p>下面以退出登录为例。控制器可以发送一个消息,在下一次请求时显示:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
def destroy
session[:current_user_id] = nil
flash[:notice] = "You have successfully logged out."
redirect_to root_url
end
end
</pre>
</div>
<p>注意,Flash 消息还可以直接在转向中设置。可以指定 <code>:notice</code>、<code>:alert</code> 或者常规的 <code>:flash</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }
</pre>
</div>
<p>上例中,<code>destroy</code> 动作转向程序的 <code>root_url</code>,然后显示 Flash 消息。注意,只有下一个动作才能处理前一个动作中设置的 Flash 消息。一般都会在程序的布局中加入显示警告或提醒 Flash 消息的代码:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<html>
<!-- <head/> -->
<body>
<% flash.each do |name, msg| -%>
<%= content_tag :div, msg, class: name %>
<% end -%>
<!-- more content -->
</body>
</html>
</pre>
</div>
<p>如此一來,如果动作中设置了警告或提醒消息,就会出现在布局中。</p><p>Flash 不局限于警告和提醒,可以设置任何可在会话中存储的内容:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<% if flash[:just_signed_up] %>
<p class="welcome">Welcome to our site!</p>
<% end %>
</pre>
</div>
<p>如果希望 Flash 消息保留到其他请求,可以使用 <code>keep</code> 方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class MainController < ApplicationController
# Let's say this action corresponds to root_url, but you want
# all requests here to be redirected to UsersController#index.
# If an action sets the flash and redirects here, the values
# would normally be lost when another redirect happens, but you
# can use 'keep' to make it persist for another request.
def index
# Will persist all flash values.
flash.keep
# You can also use a key to keep only some kind of value.
# flash.keep(:notice)
redirect_to users_url
end
end
</pre>
</div>
<h5 id="flash.now">5.2.1 <code>flash.now</code>
</h5><p>默认情况下,Flash 中的内容只在下一次请求中可用,但有时希望在同一个请求中使用。例如,<code>create</code> 动作没有成功保存资源时,会直接渲染 <code>new</code> 模板,这并不是一个新请求,但却希望希望显示一个 Flash 消息。针对这种情况,可以使用 <code>flash.now</code>,用法和 <code>flash</code> 一样:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
if @client.save
# ...
else
flash.now[:error] = "Could not save client"
render action: "new"
end
end
end
</pre>
</div>
<h3 id="cookies">6 Cookies</h3><p>程序可以在客户端存储少量数据(称为 cookie),在多次请求中使用,甚至可以用作会话。在 Rails 中可以使用 <code>cookies</code> 方法轻松获取 cookies,用法和 <code>session</code> 差不多,就像一个 Hash:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class CommentsController < ApplicationController
def new
# Auto-fill the commenter's name if it has been stored in a cookie
@comment = Comment.new(author: cookies[:commenter_name])
end
def create
@comment = Comment.new(params[:comment])
if @comment.save
flash[:notice] = "Thanks for your comment!"
if params[:remember_name]
# Remember the commenter's name.
cookies[:commenter_name] = @comment.author
else
# Delete cookie for the commenter's name cookie, if any.
cookies.delete(:commenter_name)
end
redirect_to @comment.article
else
render action: "new"
end
end
end
</pre>
</div>
<p>注意,删除会话中的数据是把键的值设为 <code>nil</code>,但要删除 cookie 中的值,要使用 <code>cookies.delete(:key)</code> 方法。</p><p>Rails 还提供了签名 cookie 和加密 cookie,用来存储敏感数据。签名 cookie 会在 cookie 的值后面加上一个签名,确保值没被修改。加密 cookie 除了会签名之外,还会加密,让终端用户无法读取。详细信息请参阅 <a href="http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html">API 文档</a>。</p><p>这两种特殊的 cookie 会序列化签名后的值,生成字符串,读取时再反序列化成 Ruby 对象。</p><p>序列化所用的方式可以指定:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Rails.application.config.action_dispatch.cookies_serializer = :json
</pre>
</div>
<p>新程序默认使用的序列化方法是 <code>:json</code>。为了兼容以前程序中的 cookie,如果没设定 <code>cookies_serializer</code>,就会使用 <code>:marshal</code>。</p><p>这个选项还可以设为 <code>:hybrid</code>,读取时,Rails 会自动返序列化使用 <code>Marshal</code> 序列化的 cookie,写入时使用 <code>JSON</code> 格式。把现有程序迁移到使用 <code>:json</code> 序列化方式时,这么设定非常方便。</p><p>序列化方式还可以使用其他方式,只要定义了 <code>load</code> 和 <code>dump</code> 方法即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
</pre>
</div>
<h3 id="渲染-xml-和-json-数据">7 渲染 XML 和 JSON 数据</h3><p>在 <code>ActionController</code> 中渲染 <code>XML</code> 和 <code>JSON</code> 数据非常简单。使用脚手架生成的控制器如下所示:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class UsersController < ApplicationController
def index
@users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render xml: @users}
format.json { render json: @users}
end
end
end
</pre>
</div>
<p>你可能注意到了,在这段代码中,我们使用的是 <code>render xml: @users</code> 而不是 <code>render xml: @users.to_xml</code>。如果不是字符串对象,Rails 会自动调用 <code>to_xml</code> 方法。</p><h3 id="过滤器">8 过滤器</h3><p>过滤器(filter)是一些方法,在控制器动作运行之前、之后,或者前后运行。</p><p>过滤器会继承,如果在 <code>ApplicationController</code> 中定义了过滤器,那么程序的每个控制器都可使用。</p><p>前置过滤器有可能会终止请求循环。前置过滤器经常用来确保动作运行之前用户已经登录。这种过滤器的定义如下:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
</pre>
</div>
<p>如果用户没有登录,这个方法会在 Flash 中存储一个错误消息,然后转向登录表单页面。如果前置过滤器渲染了页面或者做了转向,动作就不会运行。如果动作上还有后置过滤器,也不会运行。</p><p>在上面的例子中,过滤器在 <code>ApplicationController</code> 中定义,所以程序中的所有控制器都会继承。程序中的所有页面都要求用户登录后才能访问。很显然(这样用户根本无法登录),并不是所有控制器或动作都要做这种限制。如果想跳过某个动作,可以使用 <code>skip_before_action</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
</pre>
</div>
<p>此时,<code>LoginsController</code> 的 <code>new</code> 动作和 <code>create</code> 动作就不需要用户先登录。<code>:only</code> 选项的意思是只跳过这些动作。还有个 <code>:except</code> 选项,用法类似。定义过滤器时也可使用这些选项,指定只在选中的动作上运行。</p><h4 id="后置过滤器和环绕过滤器">8.1 后置过滤器和环绕过滤器</h4><p>除了前置过滤器之外,还可以在动作运行之后,或者在动作运行前后执行过滤器。</p><p>后置过滤器类似于前置过滤器,不过因为动作已经运行了,所以可以获取即将发送给客户端的响应数据。显然,后置过滤器无法阻止运行动作。</p><p>环绕过滤器会把动作拉入(yield)过滤器中,工作方式类似 Rack 中间件。</p><p>例如,网站的改动需要经过管理员预览,然后批准。可以把这些操作定义在一个事务中:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
</pre>
</div>
<p>注意,环绕过滤器还包含了渲染操作。在上面的例子中,视图本身是从数据库中读取出来的(例如,通过作用域(scope)),读取视图的操作在事务中完成,然后提供预览数据。</p><p>也可以不拉入动作,自己生成响应,不过这种情况不会运行动作。</p><h4 id="过滤器的其他用法">8.2 过滤器的其他用法</h4><p>一般情况下,过滤器的使用方法是定义私有方法,然后调用相应的 <code>*_action</code> 方法添加过滤器。不过过滤器还有其他两种用法。</p><p>第一种,直接在 <code>*_action</code> 方法中使用代码块。代码块接收控制器作为参数。使用这种方法,前面的 <code>require_login</code> 过滤器可以改写成:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
before_action do |controller|
unless controller.send(:logged_in?)
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url
end
end
end
</pre>
</div>
<p>注意,此时在过滤器中使用的是 <code>send</code> 方法,因为 <code>logged_in?</code> 是私有方法,而且过滤器和控制器不在同一作用域内。定义 <code>require_login</code> 过滤器不推荐使用这种方法,但比较简单的过滤器可以这么用。</p><p>第二种,在类(其实任何能响应正确方法的对象都可以)中定义过滤器。这种方法用来实现复杂的过滤器,使用前面的两种方法无法保证代码可读性和重用性。例如,可以在一个类中定义前面的 <code>require_login</code> 过滤器:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ApplicationController < ActionController::Base
before_action LoginFilter
end
class LoginFilter
def self.before(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in to access this section"
controller.redirect_to controller.new_login_url
end
end
end
</pre>
</div>
<p>这种方法也不是定义 <code>require_login</code> 过滤器的理想方式,因为和控制器不在同一作用域,要把控制器作为参数传入。定义过滤器的类,必须有一个和过滤器种类同名的方法。对于 <code>before_action</code> 过滤器,类中必须定义 <code>before</code> 方法。其他类型的过滤器以此类推。<code>around</code> 方法必须调用 <code>yield</code> 方法执行动作。</p><h3 id="防止请求伪造">9 防止请求伪造</h3><p>跨站请求伪造(CSRF)是一种攻击方式,A 网站的用户伪装成 B 网站的用户发送请求,在 B 站中添加、修改或删除数据,而 B 站的用户绝然不知。</p><p>防止这种攻击的第一步是,确保所有析构动作(<code>create</code>,<code>update</code> 和 <code>destroy</code>)只能通过 GET 之外的请求方法访问。如果遵从 REST 架构,已经完成了这一步。不过,恶意网站还是可以很轻易地发起非 GET 请求,这时就要用到其他防止跨站攻击的方法了。</p><p>我们添加一个只有自己的服务器才知道的难以猜测的令牌。如果请求中没有该令牌,就会禁止访问。</p><p>如果使用下面的代码生成一个表单:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @user do |f| %>
<%= f.text_field :username %>
<%= f.text_field :password %>
<% end %>
</pre>
</div>
<p>会看到 Rails 自动添加了一个隐藏字段:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
value="67250ab105eb5ad10851c00a5621854a23af5489"
name="authenticity_token"/>
<!-- username & password fields -->
</form>
</pre>
</div>
<p>所有使用<a href="form_helpers.html">表单帮助方法</a>生成的表单,都有会添加这个令牌。如果想自己编写表单,或者基于其他原因添加令牌,可以使用 <code>form_authenticity_token</code> 方法。</p><p><code>form_authenticity_token</code> 会生成一个有效的令牌。在 Rails 没有自动添加令牌的地方(例如 Ajax)可以使用这个方法。</p><p><a href="security.html">安全指南</a>一文更深入的介绍了请求伪造防范措施,还有一些开发网页程序需要知道的安全隐患。</p><h3 id="request-和-response-对象">10 <code>request</code> 和 <code>response</code> 对象</h3><p>在每个控制器中都有两个存取器方法,分别用来获取当前请求循环的请求对象和响应对象。<code>request</code> 方法的返回值是 <code>AbstractRequest</code> 对象的实例;<code>response</code> 方法的返回值是一个响应对象,表示回送客户端的数据。</p><h4 id="request-对象">10.1 <code>request</code> 对象</h4><p><code>request</code> 对象中有很多发自客户端请求的信息。可用方法的完整列表参阅 <a href="http://api.rubyonrails.org/classes/ActionDispatch/Request.html">API 文档</a>。其中部分方法说明如下:</p>
<table>
<thead>
<tr>
<th>
<code>request</code> 对象的属性</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>host</td>
<td>请求发往的主机名</td>
</tr>
<tr>
<td>domain(n=2)</td>
<td>主机名的前 <code>n</code> 个片段,从顶级域名的右侧算起</td>
</tr>
<tr>
<td>format</td>
<td>客户端发起请求时使用的内容类型</td>
</tr>
<tr>
<td>method</td>
<td>请求使用的 HTTP 方法</td>
</tr>
<tr>
<td>get?, post?, patch?, put?, delete?, head?</td>
<td>如果 HTTP 方法是 GET/POST/PATCH/PUT/DELETE/HEAD,返回 <code>true</code>
</td>
</tr>
<tr>
<td>headers</td>
<td>返回一个 Hash,包含请求的报头</td>
</tr>
<tr>
<td>port</td>
<td>请求发往的端口,整数类型</td>
</tr>
<tr>
<td>protocol</td>
<td>返回所用的协议外加 <code>"://"</code>,例如 <code>"http://"</code>
</td>
</tr>
<tr>
<td>query_string</td>
<td>URL 中包含的请求参数,<code>?</code> 后面的字符串</td>
</tr>
<tr>
<td>remote_ip</td>
<td>客户端的 IP 地址</td>
</tr>
<tr>
<td>url</td>
<td>请求发往的完整 URL</td>
</tr>
</tbody>
</table>
<h5 id="path_parameters,query_parameters-和-request_parameters">10.1.1 <code>path_parameters</code>,<code>query_parameters</code> 和 <code>request_parameters</code>
</h5><p>不过请求中的参数随 URL 而来,而是通过表单提交,Rails 都会把这些参数存入 <code>params</code> Hash 中。<code>request</code> 对象中有三个存取器,用来获取各种类型的参数。<code>query_parameters</code> Hash 中的参数来自 URL;<code>request_parameters</code> Hash 中的参数来自提交的表单;<code>path_parameters</code> Hash 中的参数来自路由,传入相应的控制器和动作。</p><h4 id="response-对象">10.2 <code>response</code> 对象</h4><p>一般情况下不会直接使用 <code>response</code> 对象。<code>response</code> 对象在动作中渲染,把数据回送给客户端。不过有时可能需要直接获取响应,比如在后置过滤器中。<code>response</code> 对象上的方法很多都可以用来赋值。</p>
<table>
<thead>
<tr>
<th>
<code>response</code> 对象的数学</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>body</td>
<td>回送客户端的数据,字符串格式。大多数情况下是 HTML</td>
</tr>
<tr>
<td>status</td>
<td>响应的 HTTP 状态码,例如,请求成功时是 200,文件未找到时是 404</td>
</tr>
<tr>
<td>location</td>
<td>转向地址(如果转向的话)</td>
</tr>
<tr>
<td>content_type</td>
<td>响应的内容类型</td>
</tr>
<tr>
<td>charset</td>
<td>响应使用的字符集。默认是 <code>"utf-8"</code>
</td>
</tr>
<tr>
<td>headers</td>
<td>响应报头</td>
</tr>
</tbody>
</table>
<h5 id="设置自定义报头">10.2.1 设置自定义报头</h5><p>如果想设置自定义报头,可以使用 <code>response.headers</code> 方法。报头是一个 Hash,键为报头名,值为报头的值。Rails 会自动设置一些报头,如果想添加或者修改报头,赋值给 <code>response.headers</code> 即可,例如:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
response.headers["Content-Type"] = "application/pdf"
</pre>
</div>
<p>注意,上面这段代码直接使用 <code>content_type=</code> 方法更直接。</p><h3 id="http-身份认证">11 HTTP 身份认证</h3><p>Rails 内建了两种 HTTP 身份认证方式:</p>
<ul>
<li>基本认证</li>
<li>摘要认证</li>
</ul>
<h4 id="http-基本身份认证">11.1 HTTP 基本身份认证</h4><p>大多数浏览器和 HTTP 客户端都支持 HTTP 基本身份认证。例如,在浏览器中如果要访问只有管理员才能查看的页面,就会出现一个对话框,要求输入用户名和密码。使用内建的身份认证非常简单,只要使用一个方法,即 <code>http_basic_authenticate_with</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class AdminsController < ApplicationController
http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end
</pre>
</div>
<p>添加 <code>http_basic_authenticate_with</code> 方法后,可以创建具有命名空间的控制器,继承自 <code>AdminsController</code>,<code>http_basic_authenticate_with</code> 方法会在这些控制器的所有动作运行之前执行,启用 HTTP 基本身份认证。</p><h4 id="http-摘要身份认证">11.2 HTTP 摘要身份认证</h4><p>HTTP 摘要身份认证比基本认证高级,因为客户端不会在网络中发送明文密码(不过在 HTTPS 中基本认证是安全的)。在 Rails 中使用摘要认证非常简单,只需使用一个方法,即 <code>authenticate_or_request_with_http_digest</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class AdminsController < ApplicationController
USERS = { "lifo" => "world" }
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_digest do |username|
USERS[username]
end
end
end
</pre>
</div>
<p>如上面的代码所示,<code>authenticate_or_request_with_http_digest</code> 方法的块只接受一个参数,用户名,返回值是密码。如果 <code>authenticate_or_request_with_http_digest</code> 返回 <code>false</code> 或 <code>nil</code>,表明认证失败。</p><h3 id="数据流和文件下载">12 数据流和文件下载</h3><p>有时不想渲染 HTML 页面,而要把文件发送给用户。在所有的控制器中都可以使用 <code>send_data</code> 和 <code>send_file</code> 方法。这两个方法都会以数据流的方式发送数据。<code>send_file</code> 方法很方便,只要提供硬盘中文件的名字,就会用数据流发送文件内容。</p><p>要想把数据以数据流的形式发送给客户端,可以使用 <code>send_data</code> 方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
require "prawn"
class ClientsController < ApplicationController
# Generates a PDF document with information on the client and
# returns it. The user will get the PDF as a file download.
def download_pdf
client = Client.find(params[:id])
send_data generate_pdf(client),
filename: "#{client.name}.pdf",
type: "application/pdf"
end
private
def generate_pdf(client)
Prawn::Document.new do
text client.name, align: :center
text "Address: #{client.address}"
text "Email: #{client.email}"
end.render
end
end
</pre>
</div>
<p>在上面的代码中,<code>download_pdf</code> 动作调用私有方法 <code>generate_pdf</code>。<code>generate_pdf</code> 才是真正生成 PDF 的方法,返回值字符串形式的文件内容。返回的字符串会以数据流的形式发送给客户端,并为用户推荐一个文件名。有时发送文件流时,并不希望用户下载这个文件,比如嵌在 HTML 页面中的图片。告诉浏览器文件不是用来下载的,可以把 <code>:disposition</code> 选项设为 <code>"inline"</code>。这个选项的另外一个值,也是默认值,是 <code>"attachment"</code>。</p><h4 id="发送文件">12.1 发送文件</h4><p>如果想发送硬盘上已经存在的文件,可以使用 <code>send_file</code> 方法。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
# Stream a file that has already been generated and stored on disk.
def download_pdf
client = Client.find(params[:id])
send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
filename: "#{client.name}.pdf",
type: "application/pdf")
end
end
</pre>
</div>
<p><code>send_file</code> 一次只发送 4kB,而不是一次把整个文件都写入内存。如果不想使用数据流方式,可以把 <code>:stream</code> 选项设为 <code>false</code>。如果想调整数据块大小,可以设置 <code>:buffer_size</code> 选项。</p><p>如果没有指定 <code>:type</code> 选项,Rails 会根据 <code>:filename</code> 中的文件扩展名猜测。如果没有注册扩展名对应的文件类型,则使用 <code>application/octet-stream</code>。</p><div class="warning"><p>要谨慎处理用户提交数据(参数,cookies 等)中的文件路径,有安全隐患,你可能并不想让别人下载这个文件。</p></div><div class="info"><p>不建议通过 Rails 以数据流的方式发送静态文件,你可以把静态文件放在服务器的公共文件夹中,使用 Apache 或其他服务器下载效率更高,因为不用经由整个 Rails 处理。</p></div><h4 id="使用-rest-的方式下载文件">12.2 使用 REST 的方式下载文件</h4><p>虽然可以使用 <code>send_data</code> 方法发送数据,但是在 REST 架构的程序中,单独为下载文件操作写个动作有些多余。在 REST 架构下,上例中的 PDF 文件可以视作一种客户端资源。Rails 提供了一种更符合 REST 架构的文件下载方法。下面这段代码重写了前面的例子,把下载 PDF 文件的操作放在 <code>show</code> 动作中,不使用数据流:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class ClientsController < ApplicationController
# The user can request to receive this resource as HTML or PDF.
def show
@client = Client.find(params[:id])
respond_to do |format|
format.html
format.pdf { render pdf: generate_pdf(@client) }
end
end
end
</pre>
</div>
<p>为了让这段代码能顺利运行,要把 PDF MIME 加入 Rails。在 <code>config/initializers/mime_types.rb</code> 文件中加入下面这行代码即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Mime::Type.register "application/pdf", :pdf