はじめに
この記事はソニックガーデン 若手プログラマ Advent Calendar 2024の4日目の記事です。
Rails8からauthenticationという認証ジェネレーターが搭載されましたね。
もし基本的な実装の内容を知らないよという方はこちらの記事をおすすめします!
実装の内容が分かりやすく書いてます。
機能も必要最低限ですし、本当にシンプルな設計ですね。
次からdeviseの代わりとして使えるのでは?と思い、今回はカスタマイズを試みました。
自分と同じように考えている方の判断材料になれば嬉しいです。
開発環境
- ruby 3.3.6
- rails 8.0.0
満たしたい要件
アカウント作成機能
- これは必須ですよね。デフォルトでは存在しません。
ログイン状態でアクセスできるアクションを制御したい
ログイン状態でアカウント作成画面、ログイン画面、パスワード変更画面にアクセスできないようにしたい
- deviseだとデフォルトでこの仕様なので、違和感がありました。ログイン状態でこれらの画面にアクセスできるアプリって存在するんでしょうか?
userとadminの2つのモデルで使えるようにしたい
-
rails g authentication
直後ではUserモデルが生成され、Userモデルだけでログインできるようになってますが、Adminというモデルでも同じように認証機能を使えるようにしたいです。
userとadminの2つのモデルでログイン状態を管理できるようにしたい
- 管理者としては同じブラウザで管理者とユーザの両方でログインしたいことって結構ありますよね。こちらも生成された時点では複数のモデルに対応させるような作りではないので、カスタマイズが必要そうです。
これらのカスタマイズを実現する上で、あまりにも変更がありそうなら、やめにします。実装コストが増えるのもそうですし、ログインやアカウント作成などの元々保証されていた機能のテストを書く羽目になるので。
設計
アカウント作成機能
ここはサインイン機能を参考にしてサクッと実装できそうです。
+ resources :users, only: %i[new create]
class UsersController < ApplicationController
allow_unauthenticated_access only: %i[new create]
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
start_new_session_for! @user
redirect_to after_authentication_url, notice: 'アカウント作成しました'
else
render :new
end
end
private
def user_params
params.require(:user).permit(:email_address, :password, :password_confirmation)
end
end
= tag.div(flash[:alert], style: "color:red") if flash[:alert]
= tag.div(flash[:notice], style: "color:green") if flash[:notice]
= form_with model: @user do |form|
= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address]
= form.password_field :password, required: true, autocomplete: "password", placeholder: "Enter your password", maxlength: 72
= form.password_field :password_confirmation, required: true, autocomplete: "password_confirmation", placeholder: "Enter your password confirmation", maxlength: 72
= form.submit "Sign up"
= link_to "Already have account?", new_session_path
ログイン状態でアクセスできるアクションを制御したい
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
+ def redirect_if_authenticated_access(**options)
+ before_action :redirect_if_authentication, **options
+ end
private
# 省略
+ def redirect_if_authentication
+ resume_session && redirect_to(root_url)
+ end
end
class UsersController < ApplicationController
allow_unauthenticated_access only: %i[new create]
+ redirect_if_authenticated_access only: %i[new create]
userとadminの2つのモデルで使えるようにしたい
こちらはポリモーフィック関連を利用して、adminモデルもuserモデルと同じようにsessionと1対多の関連付けできるようにします。
具体的な実装はこんな感じです。
- adminモデルを作成(userモデルと同じカラム)
- adminの認証用のページを作成する(userのroutes、コントローラ、ビューファイルをコピー)
- Current.userのようにCurrent.adminでadminを取得できるように
app/models/current.rb
class Current < ActiveSupport::CurrentAttributes attribute :session delegate :user, to: :session, allow_nil: true + delegate :admin, to: :session, allow_nil: true
- adminとuserをsessionsモデルと関連づける
app/models/admin.rb
class Admin < ApplicationRecord has_secure_password + has_many :sessions, as: :person, dependent: :destroy normalizes :email_address, with: ->(e) { e.strip.downcase }
app/models/user.rbclass User < ApplicationRecord has_secure_password - has_many :sessions, dependent: :destroy + has_many :sessions, as: :person, dependent: :destroy normalizes :email_address, with: ->(e) { e.strip.downcase }
app/models/session.rbclass Session < ApplicationRecord - belongs_to :admin belongs_to :person, polymorphic: true
これぐらいの変更ならまだ許容できる…?
userとadminの2つのモデルでログイン状態を管理できるようにしたい(断念)
- まずuserとadminで別のsessionで管理するようにする
- def start_new_session_for!(user)
- user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
- Current.session = session
- cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
+ def start_new_session_for!(person)
+ person.sessions.create!(person_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
+ Current.session = session
+ cookies.signed.permanent[:"session#{person.class}_id"] = { value: session.id, httponly: true, same_site: :lax }
end
end
-
require_authentication
メソッドにおいて、adminなのかuserなのか検証したいモデルを把握する必要がある- このメソッドに対して検証したい情報を外部から送る必要があるので、includedではなくclass_methodsにする
- ログアウトやログイン状態を確認するメソッドにも変更が必要になってくる…
ちょっと変更が多そうですね・・・
やめ。
まとめ
現状のauthenticationだと複数モデルに対応しておらず、かなりカスタマイズが必要そうなので、もう少しdeiviseにお世話になろうと思います。シンプルな設計であること、Railsの標準機能であることは個人的にはかなりポイントが高いので、今後のアップデートに期待してます!
最後まで読んでいただき、ありがとうございました〜!
明日は @gotchane さんが執筆予定です。お楽しみに〜!