大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
控制器
这一课你将能做到
- 写出符合 RESTful 惯例的七个控制器动作,并正确处理成功/失败两条路径
- 用 Strong Parameters 挡住批量赋值攻击,说清楚
require和permit的分工 - 用
before_action回调消除重复代码,实现登录校验和数据预加载
为什么要学
控制器是 MVC 里的「交通警察」:接住请求、调取数据、决定渲染哪个视图或跳转到哪里。它的质量直接决定应用的边界清晰度——控制器里混入业务逻辑,三个月后就是一团谁都改不动的乱麻;控制器保持「薄」,业务规则全部在模型层,代码才能长期演进。
对比本站:Spring Boot 的 @RestController 通常直接返回 JSON 给 Next.js 前端;Rails 控制器则同时承担「渲染 HTML 页面」和「处理表单提交后重定向」的职责,respond_to 还能让同一个动作按请求格式返回 HTML 或 JSON——一个控制器服务两种客户端。
先建立图像
一个标准 create 动作的「双路径」心智模型:
表单提交 POST /articles
│
▼
@article = Article.new(article_params)
│
▼
@article.save ?
╱ ╲
成功 失败
│ │
▼ ▼
redirect_to render :new,
@article status: :unprocessable_entity
(看详情页) (回到表单页显示错误)
记住这张图,90% 的 CRUD 控制器都是它的变体。
跟着做
步骤 1:生成并通读控制器
bin/rails generate controller Articles
app/controllers/articles_controller.rb:
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
def show
@article = Article.find(params[:id])
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: "文章创建成功"
else
render :new, status: :unprocessable_entity
end
end
def edit
@article = Article.find(params[:id])
end
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article, notice: "文章已更新"
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path, notice: "文章已删除", status: :see_other
end
private
def article_params
params.require(:article).permit(:title, :body)
end
end
步骤 2:理解 Strong Parameters
表单提交的数据长这样:
params = {
"authenticity_token" => "...",
"article" => { "title" => "标题", "body" => "正文", "admin" => "true" },
"commit" => "保存"
}
注意攻击者多塞了一个 admin 字段。如果没有白名单,Article.new(params[:article]) 会把 admin: true 一起写库——这就是「批量赋值漏洞」(Mass Assignment)。
article_params 方法做了两道过滤:
params.require(:article) # 必须有 :article 这个键,否则抛 400 异常
.permit(:title, :body) # 只放行 title 和 body,admin 被静默丢弃
返回的是一个「已许可」的 ActionController::Parameters 对象,才能安全传给模型。
步骤 3:用 before_action 消除重复
show/edit/update/destroy 都在重复 Article.find(params[:id]),用回调收敛:
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
def show
end
def edit
end
def update
if @article.update(article_params)
redirect_to @article, notice: "文章已更新"
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article.destroy
redirect_to articles_path, notice: "文章已删除", status: :see_other
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body)
end
end
show 和 edit 方法体直接空了——回调已经备好数据,Rails 按约定渲染同名视图。
步骤 4:一个动作同时服务 HTML 和 JSON
def index
@articles = Article.all
respond_to do |format|
format.html # 渲染 index.html.erb
format.json { render json: @articles } # /articles.json 返回 JSON
end
end
验证:
curl http://localhost:3000/articles.json
这就是 Rails 全栈模式的弹性:同一套控制器既能渲染页面,也能当 API 用。当然,纯 API 项目可以用 rails new myapp --api 生成精简版控制器基类。
步骤 5:flash 提示与状态码
redirect_to @article, notice: "保存成功" # 一次性的成功提示
redirect_to login_path, alert: "请先登录" # 一次性的警告提示
render :new, status: :unprocessable_entity # 422:表单校验失败
redirect_to articles_path, status: :see_other # 303:DELETE 后正确重定向
flash 消息只存活一次请求,显示在布局里:
<%# app/views/layouts/application.html.erb 的 body 内 %>
<% flash.each do |type, message| %>
<div class="flash flash-<%= type %>"><%= message %></div>
<% end %>
讲清楚要点
- 控制器要「薄」:动作里只做「取参数 → 调模型 → 决定响应」三件事。超过 10 行的动作就该考虑把逻辑下沉到模型或 Service 对象。
- Strong Parameters 是强制安全门:Rails 4 起强制要求,不过滤
params直接传模型会直接抛ForbiddenAttributesError,从框架层面堵死批量赋值漏洞。 - 回调三兄弟:
before_action(动作前,常用于认证和加载数据)、after_action(动作后,较少用)、around_action(包裹动作,可做耗时统计)。都支持only/except。 - render 与 redirect_to 的本质区别:render 在同一请求内直接渲染模板(URL 不变,可带实例变量);redirect_to 让浏览器发起新请求(URL 变化,实例变量丢失,靠 flash 传消息)。
- 表单校验失败要 render 不要 redirect:失败时
render :new能保留@article.errors和用户已填内容;redirect 会新开请求,错误信息和表单内容全丢。记得带status: :unprocessable_entity,否则 Turbo 下表单不工作。
你可能会懵的地方
-
params里的数据从哪来的? 三个来源合并:URL 查询串(?page=2)、表单 POST 体、路由路径段(:id)。统一用params[:key]访问,不用关心来源。 -
为什么失败时状态码用 422 而不是 200? HTTP 语义上 422(Unprocessable Entity)表示「请求格式正确但内容校验失败」。Turbo/Hotwire 依赖非 2xx 状态码来判断「该替换表单而不是跳转」,这是 Rails 7 的重要约定。
-
set_article为什么要 private? public 方法可能被当成动作(action)响应请求。回调方法、参数过滤方法都必须放private下面。 -
继承的
ApplicationController是干什么的? 所有控制器的公共基类,放全局回调(如登录校验)、异常救援(rescue_from)、helper 声明。它继承ActionController::Base。 -
控制器里能直接查数据库吗? 语法上可以,但这是典型的「胖控制器」坏味道。查询逻辑应封装在模型的 scope 或类方法里,控制器只调用语义化的接口。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
Article.new(params[:article]) 不过滤 | 批量赋值漏洞,攻击者可改任意字段 | params.require(:article).permit(...) |
四个动作各写一遍 Article.find | 重复代码,改逻辑要改四处 | before_action :set_article |
校验失败后 redirect_to new_article_path | 错误信息和已填内容全丢 | render :new, status: :unprocessable_entity |
| 回调方法写成 public | 可能被当动作直接访问 | 放 private 之下 |
| 控制器里堆 50 行业务逻辑 | 无法复用、难以测试 | 下沉到模型或 Service 对象 |
卡点急救
- 报
ActionController::ParameterMissing:表单字段名没包在article[...]里。检查表单是否用了form_with model: @article,它会自动生成正确的字段名。 - 报
ForbiddenAttributesError:直接把整个params传给了模型,改用 Strong Parameters 过滤后的结果。 - Turbo 下表单提交后页面不更新也不报错:失败分支少了
status: :unprocessable_entity,补上即可。
面试常问
-
什么是批量赋值漏洞?Rails 怎么防? 答:攻击者在表单数据里注入额外字段(如
admin=true),若服务端把整个参数哈希直接传给模型,恶意字段就会被写库。Rails 4 起强制 Strong Parameters:必须显式require顶层键、permit白名单字段,未过滤的参数传给模型直接抛异常,框架层面杜绝该漏洞。 -
render 和 redirect_to 的区别?什么场景用哪个? 答:render 在当前请求内渲染模板,URL 不变、实例变量保留,用于「展示响应」;redirect_to 返回 3xx 让浏览器发新请求,用于「操作成功后跳转」防止刷新重复提交(PRG 模式)。表单校验失败必须 render,成功必须 redirect。
-
before_action 的常见用途?要注意什么坑? 答:认证(require_login)、授权(检查资源归属)、数据预加载(set_article)。坑:回调里没 halt 链——before_action 里 redirect 或 render 后动作就不再执行,但若只设置实例变量不中断,动作照常跑,容易出意外行为。
自测三问
- create 动作的成功和失败两条路径分别用什么响应方式?为什么失败不能用 redirect?
params.require(:article).permit(:title, :body)中 require 和 permit 各自挡住什么?- before_action 里写了
redirect_to login_path,动作方法还会执行吗?
小结一句话
控制器的本分是「薄」:接参数、调模型、选响应,用 Strong Parameters 守安全门,用 before_action 去重复。
延伸
- Action Controller Overview(官方控制器指南)
- 下一课:Active Record 模型——业务规则的真正居所,验证、回调与迁移。