【Ruby on Rails】has_secure_passwordで認証機能を実装する

Ruby

こんにちは、かつコーチです。

「ログイン機能を作りたいけど、Deviseのような大きなgemを入れるほどでもない」

そんなときに便利なのが、Rails標準の機能であるhas_secure_passwordです。

パスワードのハッシュ化や照合といった面倒な処理を、bcryptと組み合わせるだけで実装できる仕組みで、自分で仕組みを理解しながら認証機能を作りたい人に向いています。

今回は、bcryptの導入からログイン機能の実装まで、手を動かしながら解説します。

has_secure_passwordの導入手順

bcryptを導入する

has_secure_passwordは、パスワードのハッシュ化にbcrypt(元のパスワードを不可逆な文字列に変換するハッシュ化アルゴリズムのgem)を利用します。

Rails 8系ではGemfileに以下の行がコメントアウトされた状態で最初から用意されているため、コメントを外すだけで使えます。

# Gemfile
gem "bcrypt", "~> 3.1.7"
bundle install

password_digestカラムを追加する

has_secure_passwordは、password_digestという名前のカラムに、ハッシュ化したパスワードを保存する仕組みです。

Userモデルを作成する場合は、以下のようにマイグレーションを用意します。

rails generate model User name:string email:string:uniq password_digest:string
rails db:migrate

生成されたマイグレーションは以下のような内容になります。

# db/migrate/xxxxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :password_digest

      t.timestamps
    end
    add_index :users, :email, unique: true
  end
end

password_digestというカラム名を守ることが重要です。

has_secure_passwordはこの名前を前提に動作するため、別の名前にすると正しく機能しません。

モデルにhas_secure_passwordを宣言する

Userモデルに1行追加するだけで、パスワード関連の機能が一通り使えるようになります。

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  validates :email, presence: true, uniqueness: true,
    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, length: { minimum: 8 }, allow_nil: true
end

この1行を追加するだけで、以下の機能が自動的に使えるようになります。

  • password=:平文のパスワードを受け取り、ハッシュ化してpassword_digestに保存する仮想属性
  • password_confirmation:確認用パスワードとの一致を自動チェックする仮想属性
  • authenticate(平文パスワード):入力されたパスワードが正しいか照合するメソッド
user = User.create!(name: "田中", email: "tanaka@example.com", password: "password123")

user.authenticate("password123")
# => Userオブジェクトが返る(認証成功)

user.authenticate("wrong-password")
# => false(認証失敗)

authenticateは認証に成功するとそのユーザー自身を、失敗するとfalseを返すため、if文の条件にそのまま使いやすい設計になっています。

ログイン機能を実装する

セッションコントローラを作成する

ログイン処理を担当するSessionsControllerを作成します。

rails generate controller Sessions new
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_path, notice: "ログインしました"
    else
      flash.now[:alert] = "メールアドレスかパスワードが正しくありません"
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_path, notice: "ログアウトしました"
  end
end

user&.authenticate(params[:password])という書き方がポイントです。

&.セーフナビゲーション演算子。レシーバがnilの場合にエラーにせずnilを返す演算子)を使うことで、そもそも該当するメールアドレスのユーザーが存在しない場合でも、NoMethodErrorを起こさずに安全にfalse相当の結果へ倒せます。

ログインフォームとルーティング

# config/routes.rb
Rails.application.routes.draw do
  resource :session, only: [:new, :create, :destroy]
  root "articles#index"
end
<!-- app/views/sessions/new.html.erb -->
<h1>ログイン</h1>

<%= form_with url: session_path, method: :post do |f| %>
  <div>
    <%= f.label :email, "メールアドレス" %>
    <%= f.email_field :email %>
  </div>
  <div>
    <%= f.label :password, "パスワード" %>
    <%= f.password_field :password %>
  </div>
  <%= f.submit "ログイン" %>
<% end %>

現在のログインユーザーを取得する

ApplicationControllerに、どのコントローラからでも使えるヘルパーメソッドを用意しておくと便利です。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
  helper_method :current_user
end

||=を使うことで、同じリクエスト内でcurrent_userが何度呼ばれても、データベースへの問い合わせは1回だけで済みます。

よくあるつまずきポイント・エラー対処

bcryptを入れ忘れてエラーになる

私が最初にこの機能を試したときに実際に出会ったのが、以下のエラーです。

❌ Before:bcryptをインストールせずhas_secure_passwordを書く

class User < ApplicationRecord
  has_secure_password
end
LoadError: Could not load the 'bcrypt' gem. Add `gem 'bcrypt'` to your Gemfile.

Gemfileのコメントアウトを外し忘れたままhas_secure_passwordだけ書いていたため、bcrypt本体が読み込めず起動時にエラーになりました。

✅ After:Gemfileのコメントを外してbundle installする

# Gemfile
gem "bcrypt", "~> 3.1.7"
bundle install

bundle installを実行してから改めてサーバーを起動すると、正常に動作するようになりました。

Rails 8系の初期Gemfileにはコメントアウトされた状態で最初から記載されているため、見落としやすいポイントです。

password_digestカラムの命名を間違える

もう一つよくあるのが、カラム名をpassword_hashなど別の名前にしてしまうケースです。

ActiveModel::MissingAttributeError: can't write unknown attribute `password_digest`

has_secure_passwordpassword_digestという名前を固定で前提にしているため、この名前以外のカラムでは動作しません。

マイグレーションを書く段階で、必ずpassword_digestという名前にすることを確認してください。

まとめ

この記事のポイント

  • has_secure_passwordはGemfileのbcryptとセットで使う、Rails標準のシンプルな認証機能
  • カラム名は必ずpassword_digestにする
  • authenticateメソッドは成功時にユーザー自身、失敗時にfalseを返す
  • user&.authenticateのようにセーフナビゲーション演算子を使うと、該当ユーザーがいない場合も安全に処理できる
  • current_userをApplicationControllerにまとめると、各コントローラから使い回せる

次に読むべき記事

→ 次の記事:Deviseで本格的な認証機能を実装する

タグ: Ruby on Rails, 中級者向け, 認証

タイトルとURLをコピーしました