Web備忘録

雑多に書いています

Ruby on Railsガイドを通読してまとめる Part.4

第四弾です。

 

コントローラー

Action Controller の概要

リクエストを受け取るコントローラーがルーティングによって指名されると、コントローラーはリクエストの意味を理解し、適切な出力を行うための責任を持つ。

これらの一連の処理はAction Controllerによって保証されている。

パラメータ(4)

一般的なWebアプリケーションと同じく、Railsでも2種類のパラメータが受け取れる

  • URLの一部としてのクエリ文字列(GET)
  • POSTデータ

JSONパラメータ(4.2)

リクエストのcontent-typeに「application/json」が指定されていたら、RailsJSONコンテンツを受け取れる。

# このJSONコンテンツは
{ "company": { "name": "acme", "address": "123 Carrot Street" } }

# パラメータでこう受け取れる
params[:company] => { "name" => "acme", "address" => "123 Carrot Street" }

ストロングパラメーター(4.5)

マスアサインメントを防ぐ技術。詳細は何度も出てくるので省略。

Strong Parametersのスコープの外のものをホワイトリスト可(許可)するコードとして、以下のようなものがある。

def product_params
  params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end

data: でスコープ外のものを指定。.try(:keys)で、中身のキーを取り出して許可、という挙動かな。

セッション(5)

セッションは遅延読み込みなので、アクセスしなかったら無効であるのと変わらない。

class ApplicationController < ActionController::Base
 
  private
 
  # キー付きのセッションに保存されたidでユーザーを検索する
  # :current_user_id はRailsアプリケーションでユーザーログインを扱う際の定番の方法。
  # ログインするとセッション値が設定され、
  # ログアウトするとセッション値が削除される。
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

session[:current_user_id]に値があるときだけ現在のユーザーを返すというメソッドの基本形。

Flash(5.2)

redirect_toにわたすことも出来る。

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 }

flashは通常、次のリクエストまでしか持たないが、keepメソッドを使用することで、保たせることが可能。

class MainController < ApplicationController
  # このアクションはroot_urlに対応しており、このアクションに対する
  # すべてのリクエストをUsersController#indexにリダイレクトしたいとします。
  # あるアクションでflashを設定してこのindexアクションにリダイレクトすると、
  # 別のリダイレクトが発生した場合にはflashは消えてしまいます。
  # ここで'keep'を使うと別のリクエストでflashが消えないようになります。
  def index
    # すべてのflash値を保持する
    flash.keep
 
    # キーを指定して値を保持することもできる
    # flash.keep(:notice)
    redirect_to users_url
  end
end

flash.nowを使えば、そのリクエストへのレンダーで評価できる。(リダイレクトで他のところに渡す場合はnowをつけない)

Cookie(6)

セッションのようにアクセス出来る。クッキーの保持期間などを設定できるらしい。

class CookiesController < ApplicationController
  def set_cookie
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
    redirect_to action: 'read_cookie'
  end
 
  def read_cookie
    cookies.encrypted[:expiration_date] # => "2014-03-20"
  end
end

クッキーの中身はユーザーごとに、ローカルにおけるそのブラウザのクッキーフォルダに保存される。

Chromeの場合はSQlite型でローカルに保存しているので、直接中身を見に行っても読み取ることは無理。(1敗)

調べた所、「Set-Cookie: クッキー名=クッキー値; expires=有効期限; domain=ドメイン名(サーバ名); path=パス; secure」というレスポンスヘッダーでクッキーをユーザーに保存させられるそう。

secureはSSL/TLS通信のときのみそのクッキーを使用するように強制する。

フィルタ(8)

before_action, after_action, around_action といったもの。

# こう設定すれば、特定のコントローラーの特定のアクションでフィルタをスキップできる
skip_before_action :require_login, only: %i(new create)

around系は、yieldで評価することで、そのアクションを実行する。

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

リクエストフォージェリからの保護(9)

CSRFからアプリケーションを守る。forgeryは「偽造」という意味。

railsではformヘルパーが自動的にトークンを作ってくれる。

ただ、フォームヘルパーを使わないケース(RailsAPI)も増えてると思うので、そのときはおそらく別の何かを用意すると思われる。

requestオブジェクト(10.1)

requestオブジェクトはリクエストに関する情報を内包している。host,domain,formatなどを使って、その内容を読み取ることが可能。

requestのパラメータに関する情報はparamsハッシュに集約してくれている。

responseオブジェクト(10.2)

responseオブジェクトはアクションが実行されるときにビルドされ、クライアントに送り返されるデータをレンダリングするため、通常はアクセスしない。

ただ、「after系」フィルタで参照できるため、セッターメソッドがあれば値を代入できる。

HTTPダイジェスト認証

https化していない状態のBASIC認証はパスワードを平文で送る。それを避けたい場合は、HTTPダイジェスト認証を使う手がある。

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

USERSという定数に「ユーザー名 => パスワード」のハッシュを与える、あとは認証のさいに入力するだけ。

ストリーミングとファイルダウンロード(12)

send_dataメソッドを使うと、クライアントにファイルをストリーミング送信できる。

サーバー上にすでにあるファイルを送りたい場合は、send_fileメソッド。ただし、Rails経由でストリーミング送信するより、Webサーバーのpublicフォルダに置いてダウンロードさせるほうがよい。

ログフィルタのカスタマイズ(14)

パラメータとリダイレクのフィルタ

# パラメータのフィルタ
config.filter_parameters << :password

# リダイレクトのフィルタ
config.filter_redirect << 's3.amazonaws.com'

パラメータのフィルタのケースは、/password/にマッチするとフィルタされる。なのでparams[:password_confirmation]の中身もフィルタ対象。

リダイレクト先のフィルタは300返したあとのlocationについてログに出さない。ヘッダーのlocationを読まれたら無意味な気もするが…。

Rescue(14)

例外ハンドリングを行い、適切なステータスを返したり、処理を行う。

rescue_fromを使えば、特定の例外(カスタム例外クラスを含む)に対して、柔軟に対応できる。

class ApplicationController < ActionController::Base
  # カスタム例外が起きたときに、with:以下のメソッドで対応
  rescue_from User::NotAuthorized, with: :user_not_authorized
 
  private
 
    def user_not_authorized
      flash[:error] = "You don't have access to this section."
      # 認証失敗した場合は、前の画面またはルートに戻る。
      redirect_back(fallback_location: root_path)
    end
end
 
class ClientsController < ApplicationController
  # ユーザーがクライアントにアクセスする権限を持っているかどうかをチェックする
  before_action :check_authorization
 
  # このアクション内で認証周りを心配する必要がない
  def edit
    @client = Client.find(params[:id])
  end
 
  private
 
    # ユーザーが認証されていない場合は単に例外をスローする
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

Railsのルーティング

Railsルーターの目的(1)

受け取ったURLを認識し、適切なアクションやRackアプリケーションに割り当てる。

浅いネスト(2.7.2)

ネストを深くしすぎるとURLが読みづらい。そこで、コレクション(index/new/createのようなidを持たないアクション)だけを、親のスコープで生成する方法がある。

:shallow オプションを付ける。

resources :articles do
  resources :comments, shallow: true
end

# 上の:shallowオプションが付いているものと同義
resources :articles do
  resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]

concernでの共通化(2.8)

ネストさせるルートに名前をつけておくような機能。

concern :commentable do
  resources :comments
end
 
concern :image_attachable do
  resources :images, only: :index
end

上で定めたconcernを

resources :messages, concerns: :commentable
 
resources :articles, concerns: [:commentable, :image_attachable]

というように使える。

memberルーティングを追加する(2.10.1)

:idをもらいながらそのコントローラにルーティングを追加したいときは

# params[:id]で取れる
resources :photos do
  member do
    get 'preview'
  end
end

# この場合はparams[:photo_id]で取る
resources :photos do
  get 'preview', on: :member
end

collectionでのルーティングはmenberと似ているが、:idをパスに置かない。

デフォルト設定を定義する(3.5)

defaults: { format: 'hoge' } でリクエストのフォーマットを定義できる。

get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }

photos/3はphotos/3.jpgというパスでのリクエストと解釈する

セグメントを制限する(3.8)

:constraintsオプションによりセグメント(:id)の値を制限できる。

get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }

高度な制限(3.10)

特定ユーザーのIPをブラックリストに入れ、ブラックリストIPはサイトに入れない設定。

matches?(request)に応答できるオブジェクトを渡せば、それを実行し、trueを返したときのみ特定のコントローラーに誘導できる。

class BlacklistConstraint
  def initialize
    @ips = Blacklist.retrieve_ips
  end
 
  def matches?(request)
    @ips.include?(request.remote_ip)
  end
end
 
Rails.application.routes.draw do
  get '*path', to: 'blacklist#index',
    constraints: BlacklistConstraint.new
end

リソースフルに制限を指定する(4.2)

/photos/:id の:idについて、constraintsに正規表現を渡すことで制限する。

resources :photos, constraints: {id: /[A-Z][A-Z][0-9]+/}

感想

とりあえず、「基礎部分」については、全て読みきりました。

クエリ発行のところが一番おもしろかったです。