第21章|今さら学ぶ「認証の実装パターン(ログイン / ログアウト)」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- セッションを使ったログインの仕組み — ホテルのチェックイン/チェックアウト
- 「remember me」の裏側 — 常連カードを発行する仕組み
- Rails 8 の組み込み認証とは何か
- Deviseは何をやっているのか — 裏方のフルセット自動化
- 権限管理(Pundit)— 「この部屋に入れる人」を決める
🏠 たとえ話で掴む「認証」
ログイン機能は ホテルのチェックイン にたとえるとスッキリ理解できます。
| ホテル | Webアプリ |
|---|---|
| フロントで身分証を見せる | メール + パスワードを送信 |
| 本人確認OK → ルームキーを渡す | 認証OK → セッション(Cookie)を発行 |
| ルームキーで部屋に入れる | Cookieを提示してログイン済みページにアクセス |
| チェックアウト → ルームキーを返す | ログアウト → セッションを削除 |
| 「常連カード」で次回チェックイン簡略化 | 「remember me」で次回ログイン省略 |
大事なのは、 ホテル側がパスワードをそのまま保管しない ことです。金庫(DB)に保管するのは暗号化された鍵(password_digest)であり、本物のパスワードは誰も見られません。
🔑 認証とは何か — 技術的な定義
そもそもHTTPは ステートレス なプロトコルです。サーバは1回のリクエストが終わると、「さっき誰と話したか」を一切覚えていません(→ 第6章)。
しかしWebアプリには「ログイン状態の維持」が必要です。記事を投稿するたびに毎回パスワードを入力させるわけにはいきません。この「ステートレスなHTTPの上で、ユーザーの状態を維持する」ために生まれた仕組みが セッション です。
認証の技術的な流れは、次の3ステップに分けられます。
- 識別(Identification) — 「あなたは誰ですか?」(メールアドレスの入力)
- 検証(Verification) — 「本当にその人ですか?」(パスワードの照合)
- 状態の維持(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次情報リンク)
- Rails ガイド:Rails セキュリティガイド(セッション管理) — セッションハイジャック対策等
- Devise(GitHub) — Deviseの公式ドキュメント
- Pundit(GitHub) — Punditの公式ドキュメント
- Rails API:has_secure_password — bcryptによるパスワードハッシュ化
まとめ
- ✅ HTTPはステートレス。ログイン状態を維持するためにセッション(Cookie)を使う
- ✅ パスワードは
has_secure_passwordでbcryptハッシュ化。ソルト付きで平文は保存しない - ✅ Rails 8 の組み込み認証は sessions テーブルでDB管理。端末ごとのログアウトが可能
- ✅ Deviseは実務で最も使われる認証Gem。モジュール式で必要な機能を選べる
- ✅ Punditで認可を実装。「誰が何をしていいか」をPolicyクラスに定義する
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに