背景
-
Rails アプリで「未ログインユーザーが認証必須ページにアクセスした際、ログイン後に元のページに戻す」機能を実装することになった
-
書籍レビューの投稿画面(ログイン必須)に未ログイン状態でアクセスした場合、ログイン後にそのレビュー投稿画面へ自動で戻す、という挙動
-
実装する中で HTTP リクエストの基礎(request オブジェクト、GET/HEAD/POST の違い、Referer ヘッダー)を深掘りしたので、設計判断の根拠とあわせてまとめる
request オブジェクトとは
ブラウザからサーバーに届いた HTTP リクエストの情報を保持するオブジェクト。Rails では ActionDispatch::Request クラスのインスタンスとしてコントローラ内で request メソッドからアクセスできる
今回の実装で使った主なメソッド:
| メソッド | 返す値 | 例 |
|---|---|---|
request.get? |
GET リクエストかどうか(true / false) |
リンククリック時 → true
|
request.head? |
HEAD リクエストかどうか(true / false) |
クローラーのアクセス時 → true
|
request.original_url |
リクエストのフル URL | http://localhost:3000/books/1/reviews/new |
request.referer |
リクエスト直前にいたページの URL | http://localhost:3000/books/1 |
HTTP メソッドの違い(GET / HEAD / POST)
GET
「ページをくれ」というリクエスト。サーバーがレスポンスヘッダー + HTML 本文を返す。リンクのクリックやアドレスバーからのアクセスで送られる
HEAD
「ページの情報だけくれ、中身はいらん」というリクエスト。サーバーがレスポンスヘッダーだけ返す(HTML 本文なし)。クローラーや監視ツールが「このページ存在する?」を確認するときに使う。人間がブラウザから手動で送ることはまずない
注意点: HEAD は GET と同じルーティングにマッピングされるが、request.get? は HEAD リクエストの時に false を返す。HEAD も安全に扱いたい場合は request.head? を明示的にチェックする必要がある
POST
フォーム送信等でサーバーにデータを送るリクエスト
Referer ヘッダー
ブラウザが「自分は今どのページにいるか」をサーバーに伝えるための HTTP ヘッダー。ブラウザがリクエスト時に自動で付与する。Rails では request.referer でアクセスできる
注意点:
- ブラウザのプライバシー設定やセキュリティポリシーによっては送られない →
nilになりうる - ブラウザが送る値なので、攻撃者が外部 URL を仕込むことも可能 → オープンリダイレクト攻撃のリスクがある
リダイレクト復帰の実装で考慮すべき3つのポイント
1. GET/POST の分岐
POST リクエストの original_url(例: /books/1/reviews)をそのまま保存すると、ログイン後に GET でアクセスした際にルーティングエラーになる。POST の URL は create アクションへのパスであり、GET でアクセスすると対応するルートが存在しないため
POST 時は referer(フォームがあったページの URL)を使うのが正しい
url = request.get? || request.head? ? request.original_url : request.referer
2. パスのみ保存してオープンリダイレクト対策
URI.parse(url).path でホスト部分を除去し、パスだけを保存する。これにより、攻撃者が Referer に外部 URL を仕込んでも、自サイト内のパスにしかリダイレクトされなくなる
session[:return_to] = URI.parse(url.to_s).path if url.present?
URI.parse は Ruby 標準ライブラリ uri のメソッドで、URL 文字列を解析して URI オブジェクトを返す。.path でパス部分(例: /books/1/reviews/new)だけを取り出せる
3. reset_session の呼び出し順序
reset_session はセッション全体を破棄するメソッド。セッション固定攻撃(Session Fixation)を防ぐためにログイン処理で呼ぶのがベストプラクティスだが、保存した session[:return_to] も一緒に消えてしまう
そのため、reset_session を呼ぶ前にローカル変数に退避しておく必要がある
return_to = session[:return_to] # 先に退避
reset_session # セッション全体を破棄
session[:user_id] = user.id # 新しいセッションにユーザーIDを保存
redirect_to return_to || root_path
実装コード
authenticate_user!(ApplicationController)
# app/controllers/application_controller.rb
def authenticate_user!
return if logged_in?
url = request.get? || request.head? ? request.original_url : request.referer
session[:return_to] = URI.parse(url.to_s).path if url.present?
redirect_to login_path, alert: t('users.sessions.require_login')
end
-
logged_in?— ログイン済みかを判定するヘルパーメソッド -
request.get? || request.head?— GET または HEAD ならoriginal_url、それ以外(POST等)ならrefererを使う。HEAD を含めているのは Brakeman(静的解析ツール)が HEAD リクエストの未考慮を指摘するため -
URI.parse(url.to_s).path— フル URL からパス部分のみ取り出す(オープンリダイレクト対策) -
t('users.sessions.require_login')— Rails のI18n.tメソッドでロケールファイルから翻訳済みメッセージを取得
SessionsController#create
# app/controllers/sessions_controller.rb
def create
user = User.authenticate_by(email: session_params[:email], password: session_params[:password])
if user
return_to = session[:return_to] # reset_session の前に退避
reset_session
session[:user_id] = user.id
redirect_to return_to || root_path, notice: t('users.sessions.logged_in')
end
end
-
User.authenticate_by— Rails 7.1 で追加されたhas_secure_passwordの認証メソッド。email とパスワードで認証し、成功すれば User オブジェクト、失敗すればnilを返す -
reset_session— セッション固定攻撃を防ぐためにセッション全体を破棄。return_toを先に退避しているのがポイント -
return_to || root_path— 戻り先が保存されていればそこへ、なければルートパスへリダイレクト
処理フロー
ユーザー操作 サーバー側の処理 session[:return_to]
─────────────────────────────────────────────────────────────────────────
レビュー投稿リンク → authenticate_user! で URL 保存 → "/books/1/reviews/new"
をクリック
ログイン画面表示 → → (そのまま保持)
ログインボタン押下 → return_to に退避 → reset_session → (消える)
→ redirect_to return_to → ログイン後に元のページへ
まとめ
-
requestオブジェクトは HTTP リクエストの情報を保持するActionDispatch::Requestのインスタンス - GET/HEAD リクエスト時は
original_url、POST 時はrefererを使ってリダイレクト先を決める -
URI.parse(url).pathでパスのみ保存することでオープンリダイレクト攻撃を防止できる -
reset_sessionはセッション全体を破棄するので、必要な値は事前に退避する
感想
- HEADリクエスト初めて知った…
- Deviseとか使わないパターンを久しぶりに実装した
参考
- ActionDispatch::Request - Rails API — request オブジェクトの全メソッド一覧
- Action Controller Overview - The Request Object(Rails Guides) — コントローラでの request の使い方
- Action Controller Overview - The Session(Rails Guides) — session と reset_session の説明
- Ruby on Rails Security Guide - Redirection and Files(Rails Guides) — オープンリダイレクト攻撃の解説
- HTTP GET(MDN) — GET メソッドの仕様
- HTTP HEAD(MDN) — HEAD メソッドの仕様
- HTTP POST(MDN) — POST メソッドの仕様
- Referer ヘッダー(MDN) — Referer の仕様と注意点
- URI(Ruby 標準ライブラリ) — URI.parse の使い方
- Brakeman - Redirect Warning — Brakeman のリダイレクト関連の警告