こんにちは、かつコーチです。
「ログイン機能を作りたいけど、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_passwordはpassword_digestという名前を固定で前提にしているため、この名前以外のカラムでは動作しません。
マイグレーションを書く段階で、必ずpassword_digestという名前にすることを確認してください。
まとめ
この記事のポイント
has_secure_passwordはGemfileのbcryptとセットで使う、Rails標準のシンプルな認証機能- カラム名は必ず
password_digestにする authenticateメソッドは成功時にユーザー自身、失敗時にfalseを返すuser&.authenticateのようにセーフナビゲーション演算子を使うと、該当ユーザーがいない場合も安全に処理できるcurrent_userをApplicationControllerにまとめると、各コントローラから使い回せる
次に読むべき記事
→ 次の記事:Deviseで本格的な認証機能を実装する
タグ: Ruby on Rails, 中級者向け, 認証