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?

第21章|今さら学ぶ「認証の実装パターン(ログイン / ログアウト)」

1
Last updated at Posted at 2026-02-25

第21章|今さら学ぶ「認証の実装パターン(ログイン / ログアウト)」

📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ

この章でわかること

  • セッションを使ったログインの仕組み — ホテルのチェックイン/チェックアウト
  • 「remember me」の裏側 — 常連カードを発行する仕組み
  • Rails 8 の組み込み認証とは何か
  • Deviseは何をやっているのか — 裏方のフルセット自動化
  • 権限管理(Pundit)— 「この部屋に入れる人」を決める

🏠 たとえ話で掴む「認証」

ログイン機能は ホテルのチェックイン にたとえるとスッキリ理解できます。

ホテル Webアプリ
フロントで身分証を見せる メール + パスワードを送信
本人確認OK → ルームキーを渡す 認証OK → セッション(Cookie)を発行
ルームキーで部屋に入れる Cookieを提示してログイン済みページにアクセス
チェックアウト → ルームキーを返す ログアウト → セッションを削除
「常連カード」で次回チェックイン簡略化 「remember me」で次回ログイン省略

大事なのは、 ホテル側がパスワードをそのまま保管しない ことです。金庫(DB)に保管するのは暗号化された鍵(password_digest)であり、本物のパスワードは誰も見られません。


🔑 認証とは何か — 技術的な定義

そもそもHTTPは ステートレス なプロトコルです。サーバは1回のリクエストが終わると、「さっき誰と話したか」を一切覚えていません(→ 第6章)。

しかしWebアプリには「ログイン状態の維持」が必要です。記事を投稿するたびに毎回パスワードを入力させるわけにはいきません。この「ステートレスなHTTPの上で、ユーザーの状態を維持する」ために生まれた仕組みが セッション です。

認証の技術的な流れは、次の3ステップに分けられます。

  1. 識別(Identification) — 「あなたは誰ですか?」(メールアドレスの入力)
  2. 検証(Verification) — 「本当にその人ですか?」(パスワードの照合)
  3. 状態の維持(Session Management) — 「以降のリクエストでもログイン済みとして扱う」(セッション+Cookie)

パスワードの照合で使われるのが bcrypt というハッシュ関数です。ハッシュ化とは、入力値を一方向に変換して元に戻せなくする処理のことです。DBにはハッシュ化された文字列(password_digest)だけを保存し、ログイン時には入力されたパスワードを同じ方法でハッシュ化して、保存済みの値と一致するかを比較します。

パスワード "secure123"
    ↓ bcryptでハッシュ化
"$2a$12$LJ3m..." ← これだけをDBに保存

ログイン時:
入力 "secure123" → ハッシュ化 → "$2a$12$LJ3m..." → DBの値と一致 → OK
入力 "wrong"     → ハッシュ化 → "$2a$12$Xk9p..." → DBの値と不一致 → NG

bcryptは「ソルト」と呼ばれるランダム文字列をハッシュに混ぜるため、同じパスワードでもユーザーごとに異なるハッシュ値が生成されます。仮にDBが漏洩しても、元のパスワードを割り出すのは極めて困難です。


🔑 セッションを使ったログインの仕組み

パスワードの保存:has_secure_password

Railsでは has_secure_password を使うことで、パスワードのハッシュ化と検証を自動で行えます。

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  # ↑ これだけで以下が使えるようになる:
  #   - password= (入力されたパスワードをハッシュ化して password_digest に保存)
  #   - authenticate(入力されたパスワードが正しいか検証)
  #   - password のpresenceバリデーション(新規作成時)
end
# ユーザー作成
user = User.create(name: "田中", email_address: "tanaka@example.com",
                   password: "secure123", password_confirmation: "secure123")
user.password_digest  # => "$2a$12$abc..." (bcryptでハッシュ化された文字列)

# パスワードの検証
user.authenticate("secure123")   # => user オブジェクト(正しい)
user.authenticate("wrong")       # => false(間違い)

ログイン〜ログアウトの流れ

① ユーザーがメール + パスワードを送信
    ↓
② サーバが User.find_by(email_address:) でユーザーを探す
    ↓
③ user.authenticate(password) でパスワードを検証
    ↓
④ 正しければセッションを作成(DBに保存)
    → Cookieにセッショントークンを書き込む
    ↓
⑤ 以降のリクエストでは Cookie のトークンで本人確認
    ↓
⑥ ログアウト時にセッションをDBから削除
    → Cookieもクリア

Rails 8 の組み込み認証

Rails 8.0 では rails generate authentication コマンドで、認証に必要なコードが自動生成されます。

$ rails generate authentication
# → 以下が生成される:
#   app/models/user.rb(has_secure_password つき)
#   app/models/session.rb
#   app/controllers/sessions_controller.rb
#   db/migrate/xxx_create_users.rb
#   db/migrate/xxx_create_sessions.rb
# app/controllers/sessions_controller.rb(Rails 8 生成コード・簡略版)
class SessionsController < ApplicationController
  def new
    # ログイン画面を表示
  end

  def create
    user = User.authenticate_by(
      email_address: params[:email_address],
      password: params[:password]
    )

    if user
      start_new_session_for(user)       # セッション作成 + Cookie設定
      redirect_to root_path, notice: "ログインしました"
    else
      flash.now[:alert] = "メールアドレスまたはパスワードが正しくありません"
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    terminate_session                    # セッション削除 + Cookieクリア
    redirect_to login_path, notice: "ログアウトしました"
  end
end

💡 Rails 7 との違い
Rails 7 以前は Cookie ストアでセッション管理し、remember_digest カラムで永続ログインを実装するのが一般的でした。Rails 8 では sessions テーブルでセッションをDB管理するため、「どの端末からログインしているか」の一覧表示や、端末ごとのログアウトが可能になります。


🎫 「remember me」の裏側

「ログイン状態を保持する」チェックボックスの裏側は、 常連カード の仕組みです。

通常のセッション   → ブラウザを閉じると消える(一時的なルームキー)
remember me       → ブラウザを閉じても残る(常連カード)

Rails 8 の組み込み認証では、セッションがDBに保存されるため、Cookieに長期間有効なトークンを保持することで remember me を実現します。


🏭 Deviseは何をやっているのか

Devise は、認証に必要な機能を「モジュール」として提供するGemです。実務で最も広く使われています。

# app/models/user.rb(Devise使用時)
class User < ApplicationRecord
  devise :database_authenticatable,   # パスワード認証
         :registerable,               # ユーザー登録
         :recoverable,                # パスワードリセット
         :rememberable,               # ログイン保持(remember me)
         :validatable,                # メール・パスワードのバリデーション
         :confirmable,                # メール確認
         :lockable                    # アカウントロック
end

Deviseの各モジュールが生成するもの:

モジュール 生成されるもの やること
database_authenticatable ログイン/ログアウト パスワード認証
registerable 登録/退会画面 ユーザー登録
recoverable パスワード再設定画面 メールでリセットリンク送信
rememberable remember me Cookie管理
confirmable メール確認画面 アカウント有効化メール
lockable ログイン失敗N回でロック

Rails 8 組み込み認証 vs Devise

Rails 8 組み込み認証 Devise
導入方法 rails g authentication gem "devise" + rails g devise:install
コード 全て可視(自分で読める) 内部に隠れている部分が多い
カスタマイズ コードを直接編集 設定 + オーバーライド
学習コスト 低い(仕組みが見える) 高い(ブラックボックス多め)
機能の豊富さ 基本的な認証のみ メール確認・ロック・OAuth等が豊富
向いている場面 新規プロジェクト、学習目的 実務(既存プロジェクトに多い)

認証機能のテストについては(→ 第22章で詳しく扱います)。


🛡️ 認可の実装(Pundit)— 「この部屋に入れる人」を決める

認証(誰か)がわかったら、次は 認可(何をしていいか) です。認証と認可は第20章でも区別しましたが、ここでは 実装パターン に焦点を当てます。

Pundit は、各アクションの権限チェックを「Policy」クラスにまとめるGemです。

# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
  # 記事を編集できるのは?
  def edit?
    record.user == user   # 著者本人だけ
  end

  # 記事を削除できるのは?
  def destroy?
    record.user == user || user.has_role?(:admin) || user.has_role?(:moderator)
    # 著者本人 or 管理者 or モデレーター
  end

  # 記事一覧を見られるのは?
  def index?
    true   # 誰でも見られる
  end
end
# コントローラでの使い方
class ArticlesController < ApplicationController
  def edit
    @article = Article.find(params[:id])
    authorize @article   # ← Pundit の権限チェック
    # → ArticlePolicy#edit? が呼ばれる
    # → false なら Pundit::NotAuthorizedError が発生
  end

  def destroy
    @article = Article.find(params[:id])
    authorize @article
    @article.destroy
    redirect_to articles_path, notice: "記事を削除しました"
  end
end
# 例外ハンドリング(application_controller.rb)
class ApplicationController < ActionController::Base
  include Pundit::Authorization

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    redirect_to root_path, alert: "この操作は許可されていません"
  end
end

🛠️ KnowledgeNoteでの具体例

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy
  has_many :user_roles, dependent: :destroy
  has_many :roles, through: :user_roles

  def has_role?(role_name)
    roles.exists?(name: role_name.to_s)
  end

  def admin?
    has_role?(:admin)
  end
end
# app/policies/comment_policy.rb
class CommentPolicy < ApplicationPolicy
  def destroy?
    record.user == user ||               # コメント投稿者
    record.article.user == user ||       # 記事の著者
    user.has_role?(:admin)               # 管理者
  end
end

💼 面接で聞かれたら?

Q:ログイン機能の仕組みを説明してください。

「HTTPはステートレスなので、状態を維持するためにセッションの仕組みを使います。ユーザーがメールとパスワードを送信すると、サーバ側でDBからユーザーを検索し、bcryptでハッシュ化されたパスワードと照合します。一致すればセッションを作成してCookieにセッショントークンを保存します。以降のリクエストではこのCookieを提示することでログイン状態が維持されます。ログアウト時にセッションを削除してCookieをクリアします。パスワードはDB上でハッシュ化されており、平文では保存しません。」

深掘りされたら:

  • 「bcryptとは?」→ パスワードのハッシュ化に使われる一方向関数。ソルト(ランダム文字列)を混ぜるため、同じパスワードでも異なるハッシュ値が生成される。仮にDBが漏洩しても元のパスワードを割り出すのが極めて困難。
  • 「Punditとは?」→ 各アクションの権限チェックをPolicyクラスにまとめるGem。コントローラで authorize を呼ぶとPolicyの対応メソッドが実行され、権限がなければ例外が発生する。
  • 「認証と認可の違いは?」→ 認証は「誰か」を判定する仕組み(Devise / Rails 8組み込み認証)。認可は「何をしていいか」をリソースごとに判断する仕組み(Pundit)。責務を分離することで保守しやすくなる。

🔗 もっと深く知りたい人へ(1次情報リンク)


まとめ

  • ✅ HTTPはステートレス。ログイン状態を維持するためにセッション(Cookie)を使う
  • ✅ パスワードは has_secure_password でbcryptハッシュ化。ソルト付きで平文は保存しない
  • ✅ Rails 8 の組み込み認証は sessions テーブルでDB管理。端末ごとのログアウトが可能
  • ✅ Deviseは実務で最も使われる認証Gem。モジュール式で必要な機能を選べる
  • ✅ Punditで認可を実装。「誰が何をしていいか」をPolicyクラスに定義する

📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに

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?