1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails 未ログイン時のリダイレクト復帰とHTTPリクエストの基礎 アウトプット

1
Posted at

背景

  • 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とか使わないパターンを久しぶりに実装した

参考

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?