mcpField
A
action-controller

action-controller

github.com/thinkoodle开发7191 次安装8 周前更新
代码仓库

关于 action-controller

面向 Rails 8.1 的 Action Controller 技能,帮助开发者和编码 Agent 编写安全、符合惯例的控制器,涵盖参数过滤、回调、响应处理与安全实践。

主要功能

  1. 01Rails 8.1 控制器惯例
  2. 02强参数及嵌套参数模式
  3. 03回调顺序与授权
  4. 04渲染、重定向和会话
  5. 05CSRF 与错误处理指南

适用场景

  1. 01编写 CRUD 控制器操作
  2. 02过滤嵌套表单参数
  3. 03添加身份验证回调
  4. 04处理错误与 Turbo 响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill action-controller

安装后在 Claude Code 中输入/action-controller确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
callbacks.md
rendering.md
security.md
sessions-and-cookies.md
streaming.md
strong-params.md

详细介绍

Rails Action Controller 专家

遵循 Rails 8.1 规范,编写正确、安全且符合惯例的 Rails 控制器。

理念

  1. 保持控制器精简——业务逻辑应放在模型或服务中,而非控制器中
  2. 强参数可防止批量赋值——原始参数会让攻击者设置任意属性(管理员标记、用户 ID 等)
  3. 约定优于配置——遵循 RESTful 模式;克制添加自定义操作的冲动
  4. 默认安全失败——默认限制访问,再有选择地开放
  5. 一个控制器对应一个资源——如果控制器处理两个资源,就拆分它

何时使用此技能

  • 编写新的控制器操作(CRUD 或自定义操作)
  • 允许参数(尤其是嵌套哈希和数组——这类地方最容易出错)
  • 添加用于身份验证/授权的 before_action 过滤器
  • 设置用于错误处理的 rescue_from
  • 处理会话、Cookie 或 Flash 消息
  • 渲染响应或重定向
  • 实现流式传输或文件下载
  • 配置 CSRF 保护或 HTTP 身份验证

说明

第 1 步:检查现有模式

先查看项目现有的控制器——与代码库保持一致比照搬教科书模式更重要:

# See what ApplicationController provides
cat app/controllers/application_controller.rb

# Find similar controllers
ls app/controllers/

# Check for shared concerns
ls app/controllers/concerns/

# Check routes for the resource
bin/rails routes | grep resource_name

一致性胜过“最佳实践”。

第 2 步:控制器结构

每个控制器内部都遵循以下顺序:

class ArticlesController < ApplicationController
  # 1. Includes/concerns
  include Searchable

  # 2. Constants (if any)
  ITEMS_PER_PAGE = 25

  # 3. Callbacks — order matters, they run top-to-bottom
  before_action :authenticate_user!
  before_action :set_article, only: [:show, :edit, :update, :destroy]
  before_action :authorize_article, only: [:edit, :update, :destroy]

  # 4. Public actions (RESTful order: index, show, new, create, edit, update, destroy)
  def index
    @articles = Article.all
  end

  def show; end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: "Article created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    if @article.update(article_params)
      redirect_to @article, notice: "Article updated."
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article.destroy!
    redirect_to articles_path, notice: "Article deleted.", status: :see_other
  end

  # 5. Private methods
  private

    def set_article
      @article = Article.find(params.expect(:id))
    end

    def authorize_article
      redirect_to articles_path, alert: "Not authorized." unless @article.user == current_user
    end

    def article_params
      params.expect(article: [:title, :body, :published])
    end
end
第 3 步:强参数

大多数控制器错误都源于此处——尤其是嵌套哈希和数组。

使用 expect(Rails 8+),而非 require + permit
# ✅ Rails 8+ — prefer expect (combines require + permit)
def article_params
  params.expect(article: [:title, :body, :published])
end

# ⚠️ Older style — still works but expect is preferred
def article_params
  params.require(:article).permit(:title, :body, :published)
end
嵌套哈希——危险地带
# Nested hash (belongs_to address, has fields)
# Form sends: { user: { name: "Jo", address: { street: "123 Main", city: "NY" } } }
def user_params
  params.expect(user: [:name, address: [:street, :city, :zip]])
end

# ⚠️ Wrong — this permits NOTHING inside address
def user_params
  params.expect(user: [:name, :address])  # address is a hash, not a scalar!
end
标量数组
# Array of simple values: { article: { title: "Hi", tag_ids: [1, 2, 3] } }
def article_params
  params.expect(article: [:title, tag_ids: []])
end
哈希数组(accepts_nested_attributes_for)
# Array of nested objects — NOTE THE DOUBLE ARRAY SYNTAX [[...]]
# { project: { name: "X", tasks_attributes: [{ title: "A" }, { title: "B" }] } }
def project_params
  params.expect(project: [:name, tasks_attributes: [[:title, :done, :id, :_destroy]]])
end

双重数组 [[...]] =“我希望得到一个哈希数组,其中每个哈希都包含这些键。” 这是 Rails 8 的 expect 语法。Agent 经常弄错这一点。

任意哈希(谨慎使用)
# When you genuinely can't enumerate keys (e.g., JSON metadata blob)
def product_params
  params.expect(product: [:name, metadata: {}])
end
# ⚠️ metadata: {} permits ANY keys — only use when truly dynamic
常见陷阱
错误修复方法
标签是数组时使用 permit(:tags)使用 permit(tags: [])
地址是哈希时使用 permit(:address)使用 permit(address: [:street, :city])
上传文件时使用 permit(:images)多个文件使用 permit(images: [])
嵌套属性未包含 _destroy 和 id使用 permit(items_attributes: [[:name, :id, :_destroy]])
使用 permit! 来“让它先能用”明确列出参数——permit! 允许攻击者设置任意属性
第 4 步:回调(before_action、after_action、around_action)
before_action
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # Skip inherited callbacks selectively
  skip_before_action :authenticate_user!, only: [:index, :show]

  private
    def set_post
      @post = Post.find(params.expect(:id))
    end
end

关键规则:

  • 回调按声明顺序运行——先进行身份验证,再加载资源
  • 执行渲染或重定向的回调会中止调用链(后续回调和操作都不会运行)
  • 使用 only: / except: 将回调限定到特定操作
  • skip_before_action 仅适用于从父类继承或更早注册的回调
around_action
around_action :wrap_in_transaction, only: [:create, :update]

private
  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      yield  # executes the action
    end
  end

必须调用 yield——否则操作永远不会执行。

第 5 步:渲染响应
# Implicit render — renders app/views/posts/index.html.erb
def index
  @posts = Post.all
  # no explicit render needed
end

# Explicit render of a different template
render :new                           # same controller, different action template
render "posts/new"                    # cross-controller template
render plain: "OK"                    # plain text
render json: @post                    # JSON
render json: @post, status: :created  # JSON with status
render html: "<h1>Hi</h1>".html_safe  # raw HTML
render inline: "<%= 'hi' %>"          # inline ERB (avoid)
render nothing: true, status: :ok      # empty body (deprecated — use head)

# head — response with no body
head :no_content          # 204
head :created, location: post_url(@post)

# Render with status (Turbo requires it for error responses)
render :new, status: :unprocessable_entity   # 422 — required for Turbo
render :edit, status: :unprocessable_entity

**对于 Turbo/Hotwire:**表单提交失败时需要设置 status: :unprocessable_entity(422)——否则 Turbo 会忽略响应,用户看不到错误反馈。

用于多种格式的 respond_to
def show
  @post = Post.find(params.expect(:id))

  respond_to do |format|
    format.html  # renders show.html.erb
    format.json { render json: @post }
    format.pdf { send_data generate_pdf(@post), filename: "post.pdf" }
  end
end
第 6 步:重定向
redirect_to @post                          # redirect to show
redirect_to posts_path                     # redirect to index
redirect_to root_path, notice: "Done!"     # with flash notice
redirect_to root_path, alert: "Oops!"      # with flash alert
redirect_to root_path, status: :see_other  # 303 — use for DELETE actions

# Redirect back (with fallback)
redirect_back fallback_location: root_path

# ⚠️ After DELETE, use status: :see_other (303)
# This prevents browsers from replaying the DELETE on redirect
def destroy
  @post.destroy!
  redirect_to posts_path, notice: "Deleted.", status: :see_other
end
第 7 步:Flash 消息
# Set in redirect
redirect_to @post, notice: "Saved!"
redirect_to @post, alert: "Problem!"

# Set manually (available on NEXT request)
flash[:notice] = "Saved!"
flash[:alert] = "Problem!"

# Set for CURRENT request (use when rendering, not redirecting)
flash.now[:error] = "Could not save."
render :new, status: :unprocessable_entity

# Carry flash through an extra redirect
flash.keep
redirect_to another_path

**经验法则:**在 redirect_to 前使用 flash[...]。在 render 前使用 flash.now[...]。

第 8 步:会话和 Cookie
会话
# Store
session[:current_user_id] = user.id

# Read
session[:current_user_id]

# Delete
session.delete(:current_user_id)

# Nuke everything (do this on login to prevent session fixation)
reset_session
Cookie
# Basic (deleted when browser closes)
cookies[:theme] = "dark"

# With expiration
cookies[:theme] = { value: "dark", expires: 1.year }

# Permanent (20 years)
cookies.permanent[:locale] = "en"

# Signed (tamper-proof, but readable)
cookies.signed[:user_id] = current_user.id
cookies.signed[:user_id]  # => 42

# Encrypted (tamper-proof AND unreadable)
cookies.encrypted[:token] = "secret"
cookies.encrypted[:token]  # => "secret"

# Delete
cookies.delete(:theme)
第 9 步:使用 rescue_from 处理错误
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActiveRecord::RecordInvalid, with: :unprocessable
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private
    def not_found
      respond_to do |format|
        format.html { render file: Rails.root.join("public/404.html"), status: :not_found, layout: false }
        format.json { render json: { error: "Not found" }, status: :not_found }
      end
    end

    def unprocessable(exception)
      respond_to do |format|
        format.html { redirect_back fallback_location: root_path, alert: exception.message }
        format.json { render json: { error: exception.message }, status: :unprocessable_entity }
      end
    end

    def bad_request(exception)
      render json: { error: exception.message }, status: :bad_request
    end
end

不要捕获 Exception 或 StandardError——它们会捕获 SystemExit 和 SyntaxError 等异常,破坏 Rails 内部机制并掩盖真正的错误。

第 10 步:CSRF 保护

默认启用。要点如下:

# Default — raises exception on CSRF failure
protect_from_forgery with: :exception

# For API controllers — skip CSRF (use token auth instead)
class Api::BaseController < ActionController::API  # no CSRF by default
end

Turbo 和 rails-ujs 会自动处理 CSRF。对于自定义的 fetch 调用,从 <meta name="csrf-token"> 标签读取令牌,并将其作为 X-CSRF-Token 标头发送。

第 11 步:HTTP 身份验证、流式传输、请求/响应
# Basic Auth — quick and dirty (admin panels, staging)
http_basic_authenticate_with name: "admin", password: ENV["ADMIN_PASSWORD"]

# Token Auth — for APIs
authenticate_or_request_with_http_token do |token, _options|
  ActiveSupport::SecurityUtils.secure_compare(token, ENV["API_TOKEN"])
end

# Send generated data as download
send_data pdf_content, filename: "report.pdf", type: "application/pdf"

# Send existing file
send_file Rails.root.join("storage/report.pdf"), filename: "report.pdf"

# Live streaming — always close the stream
include ActionController::Live
response.headers["Content-Type"] = "text/event-stream"
response.stream.write "data: hello\n\n"
ensure
  response.stream.close

# Useful request properties
request.remote_ip          # client IP
request.get? / request.post?  # method checks
request.headers["X-Custom"]
request.format             # Mime::HTML, Mime::JSON, etc.
request.variant = :mobile  # for device-specific views

应避免的反模式

  1. 臃肿的控制器 — 将业务逻辑移至模型、服务或表单对象
  2. permit! — 会为批量赋值攻击打开方便之门;应逐一列出所有允许的属性
  3. 在模型调用中直接使用 params[:foo] — 会绕过强参数过滤;应通过 params 方法处理
  4. 大范围跳过 CSRF 防护 — 仅对使用令牌身份验证的真正 API 端点跳过;CSRF 可防止跨站表单提交
  5. rescue_from Exception — 会捕获语法错误、SystemExit 以及其他不应被吞掉的异常
  6. 在操作中使用嵌套的 if/else 链 — 提取到服务对象中
  7. 多次渲染或重定向 — 控制器操作只能渲染或重定向一次;使用 and return 或提前返回
  8. 错误渲染时未指定状态 — Turbo 要求使用 status: :unprocessable_entity
  9. 所有内容都放进会话 — 使用 CookieStore 时,会话最大为 4KB;大型数据应存入数据库
  10. DELETE 后重定向时未使用 :see_other — 可能导致浏览器重放 DELETE 请求

快速参考

详细模式和示例请参阅 references/ 目录:

  • references/strong-params.md — 完整的参数许可模式(嵌套哈希、数组、双数组语法)
  • references/callbacks.md — 所有回调类型、排序规则、中止方式、跳过模式
  • references/rendering.md — 完整的渲染选项、重定向、闪存消息、respond_to、变体
  • references/sessions-and-cookies.md — 会话存储、Cookie Jar 类型、配置
  • references/security.md — rescue_from、CSRF、HTTP 身份验证、CSP、日志过滤、强制 SSL、浏览器版本控制
  • references/streaming.md — send_data/send_file、SSE 流式传输、请求/响应对象、API 控制器、运行状况检查

评论

支持 Markdown