LoginSignup
10

More than 5 years have passed since last update.

基礎Ruby on Rails Chapter11 ストロング・パラメータ/エラーページのカスタマイズ

Last updated at Posted at 2018-10-13

基礎Ruby on Rails Chapter11 モデル間の関連付け/ブログ機能の追加(コントローラ/ビュー編)
基礎Ruby on Rails Chapter12 アセット・パイプライン

ストロング・パラメータとは

マスアサインメント脆弱性

  • 例えば、編集ページを開いて、HTMLソースのaccount[member]account[administrator]に書き換えて、更新ボタンを押せば管理者に変更できてしまう。これは重大なセキュリティホールで、マスアサインメント脆弱性と呼ばれる。

ストロング・パラメータの有効化

  • ストロング・パラメータは、マスアサインメント脆弱性への対策として、Rails4.0から導入された仕組み。
  • 現在は学習のためにストロング・パラメータを無効化しているので、有効化する。
config/application.rb(一部)
    # false=ストロング・パラメータの有効化
    config.action_controller.permit_all_parameters = false

ストロング・パラメータの有効にした状態で更新するとエラーが出る。

image.png

ストロング・パラメータの使い方

  • assign_attributesの所に、.permit(:member, :name,・・・を追加することにより、余計な項目を保存しないようにする
  def update
    @member = current_member
    @member.assign_attributes(params[:account].permit(:member, :name, :full_name, :sex, :birthday, :email))
    if @member.save
      redirect_to :account, notice: "アカウント情報を更新しました。"
    else
      render "edit"
    end
  end

プライベートメソッドを活用する

  • さらに上記のassign_attributesの引数部分をプライベートメソッドにすると、ごちゃごちゃしない。
  #プライベートメソッド
  private def account_params
    params[:account].permit(:member, :name, :full_name, :sex, :birthday, :email)
  end

  #以下のように呼び出しができる。
  @member.assign_attributes(account_params)

requireメソッド

  • 上記で、:accountキーが含まれていない場合、NoMethodErrorが発生してしまう。
  • params.require(:account).permitとすれば、:accountがない時は、ParameterMissingとなる。
  • エラーの種類としては、NoMethodErrorはバグなので、ParameterMissingのほうが望ましい。よって、requireを使うべきである。
  private def account_params
    params.require(:account).permit(:member, :name, :full_name, :sex, :birthday, :email)
  end

コントローラの修正

マイアカウントページのストロング・パラメータ対応

  • プライベートメソッドaccount_paramsを作成し、updateのパラメータに入れる。
app/controllers/accounts_controller.rb(一部)
  def update
    @member = current_member
    @member.assign_attributes(account_params)
    if @member.save
      redirect_to :account, notice: "アカウント情報を更新しました。"
    else
      render "edit"
    end
  end

  private def account_params
    params.require(:account).permit(
        :number,
        :name,
        :full_name,
        :sex,
        :birthday,
        :email
    )
  end

会員情報のストロング・パラメータ対応

  • プライベートメソッドmember_paramsを作成し、createとupdateのパラメータに入れる。
app/controllers/members_controller.rb(一部)
  def create
    @member = Member.new(member_params)
    if @member.save
      # 保存が成功したらshowにリダイレクトする。フラッシュ値を設定する。
      redirect_to @member, notice: "会員を登録しました。"
    else
      # エラー発生時はnewに戻る
      render "new"
    end
  end

  def update
    @member = Member.find(params[:id])
    @member.assign_attributes(member_params)
    if @member.save
      # 保存が成功したらshowにリダイレクトする。フラッシュ値を設定する。
      redirect_to @member, notice: "会員を更新しました。"
    else
      # エラー発生時はeditに戻る
      render "edit"
    end
  end

  private def member_params
    attrs = [
        :number,
        :name,
        full_name,
        :sex,
        :bitthday,
        :email,
        :administrator
    ]
    # createの時は、:passwordを追加する
    attrs << :password if params[:action] == "create"

    params.require(:member).permit(attrs)
  end

パスワード変更機能のストロング・パラメータ対応

  • プライベートメソッドaccount_paramsを作成し、updateのパラメータに入れる。
  • また、current_password = params[:account][:current_password]も、current_password = account_params[:current_password]にする。
app/controllers/passwords_controller.rb(一部)
  def update
    @member = current_member
    current_password = account_params[:current_password]

    if current_password.present?
      if @member.authenticate(current_password)
        @member.assign_attributes(account_params)
        if @member.save
          redirect_to :account, notice: "パスワードを変更しました。"
        else
          render "edit"
        end
      else
        @member.errors.add(:current_password, :wrong)
        render "edit"
      end
    else
      @member.errors.add(:current_password, :empty)
      render "edit"
    end
  end

  private def account_params
    params.require(:account).permit(
        :current_password,
        :password,
        :password_confirmation
    )
  end

ニュース記事管理機能のストロング・パラメータ対応

  • プライベートメソッドarticle_paramsを作成し、createとupdateのパラメータに入れる。
app/controllers/articles_controller.rb(一部)
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: "ニュース記事を登録しました。"
    else
      render "new"
    end
  end

  def update
    @article = Article.find(article_params)
    @article.assign_attributes(params[:article])
    if @article.save
      redirect_to @article, notice: "ニュース記事を更新しました。"
    else
      render "edit"
    end
  end

  private def article_params
    params.require(:article).permit(
        :title,
        :body,
        :released_at,
        :no_expiration,
        :expired_at,
        :member_only
    )
  end

ブログ記事管理機能のストロング・パラメータ対応

  • プライベートメソッドentry_paramsを作成し、createとupdateのパラメータに入れる。
app/controllers/entries_controller.rb(一部)
  def create
    @entry = Entry.new(entry_params)
    @entry.author = current_member
    if @entry.save
      redirect_to @entry, notice: "記事を作成しました。"
    else
      render "new"
    end
  end

  def update
    @entry = current_member.entries.find(params[:id])
    @entry.assign_attributes(entry_params)
    if @entry.save
      redirect_to @entry, notice: "記事を更新しました。"
    else
      render "edit"
    end
  end

  private def entry_params
    params.require(:entry).permit(
        :member_id,
        :title,
        :body,
        :posted_at,
        :status
    )
  end
  • いくつかの画面を更新したところ、エラーが出ることなく保存できることを確認した。

エラーページのカスタマイズ

Railsの例外を処理する

  • Railsアプリケーションの起動中に例外が発生したときは、Railsが自動的に例外を捕捉してエラー表示を行う。
  • エラー表示をカスタマイズしたい場合は、コントローラの中でクラスメソッドrescue_fromメソッドを使用して、自分で例外を処理する。
  • rescue_fromの第1引数は、例外のクラス名。withオプションには例外が発生した時に実行するメソッドの名前を指定する。
  • 上の3つは、本番モードの時だけ処理している。
  • 親子関係にある例外クラスは、親のほうを先に指定する。StandardErrorは親(の親)。
app/controllers/application_controller.rb(一部)
  if Rails.env.production? || ENV["RESCUE_EXCEPTIONS"]
    rescue_from StandardError, with: :rescue_internal_server_error
    rescue_from ActiveRecord::RecordNotFound, with: :rescue_not_found
    rescue_from ActionController::ParameterMissing, with: :rescue_bad_request
  end

  rescue_from LoginRequired, with: :rescue_login_required
  rescue_from Forbidden, with: :rescue_forbidden

#(省略)

  private def rescue_bad_request(exception)
    render "error/bad_request", status: 400, layout: "error", formats: [:html]
  end

  private def rescue_login_required(exception)
    render "error/login_required", status: 403, layout: "error", formats: [:html]
  end

  private def rescue_forbidden(exception)
    render "error/forbidden", status: 403, layout: "error", formats: [:html]
  end

  private def rescue_not_found(exception)
    render "error/not_found", status: 404, layout: "error", formats: [:html]
  end

  private def rescue_internal_server_error(exception)
    render "error/internal_server_error", status: 500, layout: "error", formats: [:html]
  end

エラー用テンプレート

テンプレートの作成

app/views/layouts/error.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= page_title %></title>
    <%= stylesheet_link_tag "application", media: "all" %>
  </head>
  <body>
    <div id="container">
      <header>
        <%= image_tag("logo.gif", size: "272x48", alt: "Morning Glory") %>
        <nav class="menubar">
          <a href="/">TOP</a>
        </nav>
      </header>
      <main>
        <%= yield %>
      </main>
      <footer>
        <%= render "shared/footer" %>
      </footer>
    </div>
  </body>
</html>
app/views/errors/bad_request.html.erb
<% @page_title = "Bad Request" %>
<h1>400 Bad Request</h1>
<p>リクエストの形式が正しくありません。</p>
app/views/errors/login_required.html.erb
<% @page_title = "ログインが必要です" %>
<h1><%= @page_title %></h1>

<%= render "shared/login_form" %>
app/views/errors/forbidden.html.erb
<% @page_title = "Forbidden" %>
<h1>403 Forbidden</h1>
<p>このページにはアクセスできません。</p>
app/views/errors/not_found.html.erb
<% @page_title = "Not Found" %>
<h1>404 Not Found</h1>
<p>ページが見つかりません。</p>
app/views/errors/internal_server_error.html.erb
<% @page_title = "Internal Server Error" %>
<h1>500 Internal Server Error</h1>
<p>システムエラーのためページが表示できません。</p>

エラーページの確認

  • config/environmentsディレクトリのdevelop.rbに、consider_all_requests_localという設定項目がある
    • true・・・エラーをブラウザに表示をする。
    • false・・・ブラウザには詳細情報が表示されない。
  • falseに設定する。
config/environments/development.rb(一部)
  # Show full error reports.
  config.consider_all_requests_local = false
  • 上のほうで、以下のような部分があり、環境変数ENV["RESCUE_EXCEPTIONS"]がtrueであれば、if内のエラー表示がされるようになる。
app/controllers/application_controller.rb(一部)
  if Rails.env.production? || ENV["RESCUE_EXCEPTIONS"]
    rescue_from StandardError, with: :rescue_internal_server_error
    rescue_from ActiveRecord::RecordNotFound, with: :rescue_not_found
    rescue_from ActionController::ParameterMissing, with: :rescue_bad_request
  end
  • 以下のようにしてサーバーを起動すると、環境変数ENV["RESCUE_EXCEPTIONS"]=1がセットされて、本番モードと同様にすべての例外が捕捉されるようになる。
$ RESCUE_EXCEPTIONS=1 bin/rails s
  • http://localhost:3000/membersを開くと、ログインフォームが表示される。

image.png

  • http://localhost:3000/articles/100を開くと、404エラーが表示される。

image.png

  • 他のエラーは出すのが大変なので、top_controllerに、bad_requestforbiddeninternal_server_errorメソッドを加えて、呼び出す。
app/controllers/top_controller.rb(一部)
  def bad_request
    raise ActionController::ParameterMissing, ""
  end

  def forbidden
    raise Forbidden, ""
  end

  def internal_server_error
    raise
  end
  • ルーティングにも、bad_requestforbiddeninternal_server_errorを追加する。
config/routes.rb(一部)
Rails.application.routes.draw do
  root "top#index"
  get "about" => "top#about", as: "about"
  get "bad_request" => "top#bad_request"
  get "forbidden" => "top#forbidden"
  get "internal_server_error" => "top#internal_server_error"
  • それぞれ、http://localhost:3000/bad_requesthttp://localhost:3000/forbiddenhttp://localhost:3000/internal_server_errorを呼び出す。

image.png
image.png
image.png

ルーティングエラーの処理

  • ルーティングエラーは、ApplicationControllerを書き換えただけでは処理できない。
  • Action Dispatchがルーティングエラーを処理する。
  • Action Controllerに伝わらない例外を処理する方法として、ErrorControllerのshowアクションを呼ぶように指定する。
config/application.rb(一部)
    config.exceptions_app = ->(env) do
      ErrorsController.action(:show).call(env)
    end
  • 新規ファイル、errors_controller.rbを新規作成する。
app/controllers/errors_controller.rb
class ErrorsController < ActionController::Base
  layout "error"

  def show
    case request.env["action_dispatch.exception"]
    when ActionController::RoutingError
      render "not_found", status: 404, formats: [:html]
    else
      render "internal_server_error", status: 500, formats: [:html]
    end
  end
end

  • http://localhost:3000/xxxxxxxxxxxxxのような変なアドレスにしても適切に処理される。

image.png

  • config/environments/development.rbconfig.consider_all_requests_local = trueに元に戻して、終了。

まとめ

  • ストロング・パラメータを設定することにより、画面に表示されていない項目の更新を防ぐ。
  • 様々なエラーを画面に適切に出力することにより、実用的なWebサイトを構築できる。

参考
改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10