Zennに投稿した記事の転載です。
https://zenn.dev/kitabatake/articles/start-to-like-the-devise
この記事は、Railsのユーザー認証機能のgemとして有名なDeviseを使っていて感じがちな、モヤモヤを解消することを目的としています。
私自身がモヤモヤを感じていたため、このようなタイトルにしましたが、単に「Deviseに対する理解を深める」目的で読んでいただければと思います。
Devise はとても便利ですよね。
installして、configファイルを設置して、モデルの設定をして、routes.rbにdevise_for
を書くだけで導入できます。
たったこれだけで、、
- ユーザーの新規登録画面の作成
- ログイン画面の作成
- パスワードを忘れた際の画面の作成
-
authenticate_user!
やuser_signin?
のようなユーザー認証向けの各種メソッドの付与 - etc...
といったユーザー認証機能に関する様々なことをしてくれます。凄いです。
「最低限の設定だけで、多くのことをやってくれること」の代償として、「Deviseがやってくれることの全容が掴みづらい」という点があると思います。
「ユーザー認証」というプロジェクトにおいて大事な部分を担っているのに、その全容が掴めていない状態というのはモヤモヤしてしまいますよね。
もちろん、ちゃんと調べたら解るのですが、ちゃんと調べるのは根気と時間がいるので、モヤモヤを放置してしまっている方も多いのではないでしょうか。
また、Deviseは様々なプロジェクトに導入できるように、かなり柔軟な設定ができるようになっています。
柔軟な設定を可能にするためには、内部では複雑な仕組みが必要です。
そのため、ちょっとした調整作業であっても、内部の処理を把握する必要がある作業は、難しくなってしまいます。
このような、手触り感がなく、ブラックボックス感を感じてしまうことも、モヤモヤを感じてしまう点だと思います。
この2つの 「全容を掴みづらい」 と 「ブラックボックス感がある」 のモヤモヤの原因を、次のようなアプローチで解消することを試みます。
- 前提知識として「Deviseにとってユーザー認証する」とは何なのかを明確にする
- Deviseがやってくれる仕事を具体的に知ることで、Deviseの全容を掴む
- Deviseの内側を把握することで、ブラックボックス感を解消する
それでは、順に見ていきましょう。
Deviseにとって「ユーザーを認証する」とは
まず前提知識として、Deviseにとって「ユーザーを認証する」とはどういう意味なのかを明確にしておきたいと思います。
一般的なWEBサービスにおいてユーザー認証とは、「アクセスしてきたユーザーが何者かを確認すること」と表現できると思います。
Deviseにとって「何者」に当たるのは「モデルのレコード」です。
したがって、Deviseにとって「ユーザーを認証する」ということは、 「アクセスしてきたユーザーがどのモデルのどのレコードかを確認する」 と表現できます。
「どのモデルか」は、認証画面のURLによって決まります。
「どのレコードか」は、デフォルトではemail
とpassword
を使って照合します。
(照合する方法は他にもありますが、ここでは細かい説明は不要という意図で、省略します)
また、認証対象のモデルを複数設定できるのが、Deviseの柔軟な部分です。
仮に、UserモデルとCustomerモデルを認証用に使いたい場合は、routes.rbに次のように記述します。
devise_for :users
devise_for :customers
この場合、/users/sign_in
がUserモデルの認証画面のURLとなり、/customers/sign_in
がCustomerモデルの認証画面のURLとなります。
まとめますと、
- Deviseにとって「ユーザーを認証する」とは 「ユーザーがどのモデルのどのレコードかを確認する」 こと
- 認証対象のモデルは複数設定でき、ユーザーをどのモデルとして認証するかはURLによって決まる
Deviseを理解する上で、この2点を把握しておくことが大事だと思ったので冒頭に説明しました。
Deviseの仕事の全容を把握する
Deviseはユーザー認証機能を実装する上で必要な仕事を 色々 やってくれます。
この 「色々」 の部分を具体的に知る事で、モヤモヤの原因の1つでもある「全容を掴みづらい」を解消する狙いです。
Deviseがやってくれる仕事は大きく分けると次の3つがあります。
- ユーザー認証の基本的なメソッドの付与
- 認証用モデルごとのhelper系メソッドの作成
- モジュールごとに必要な仕事
1. ユーザー認証の基本的なメソッドの付与 は、Deviseをinstallした時点でApplicationControllerに対して、sign_in
メソッドなどの、ユーザー認証の機能に必要な基本的なメソッドを付与してくれます。
2. 認証用モデルごとのhelper系メソッドの作成 は、routes.rbファイルのdevise_for
が呼び出された時点で、current_user
メソッドのなどの、認証用モデルの名前が入ったメソッドを作成してくれます。
3. モジュールごとに必要な仕事 は、routes.rbファイルのdevise_for
メソッドと、モデル内のdevise
メソッドのモジュールの設定に応じて、それぞれのモジュールの機能を実現するために必要な仕事をしてくれます。
それぞれ詳しく見ていきましょう。
ユーザー認証の基本的なメソッドの付与
Deviseのgemをinstallするだけで、ApplicationControllerにユーザー認証用の基本的なメソッドを付与してくれます。
数が多いので全ては紹介できませんが、どういったものが付与されるかがイメージ出来るように、いくつかピックアップしてみます。
sign_in
ユーザーとモデルのインスタンスを紐づける(ログイン)
sign_in(:user, @user)
sign_out
ユーザーとモデルのインスタンスの紐付けを解除する(ログアウト)
sign_out(:user)
after_sign_in_path_for
ログイン後にリダイレクトするパスを取得する
after_sign_in_path_for(:user)
もっと詳しく知りたい方は、下記のディレクトリを見てみてください。
https://github.com/heartcombo/devise/tree/dfbed22cee617992e5f846fdef58b724b6b32ff9/lib/devise/controllers
このディレクトリにある全てのModuleがApplicationController
にincludeされます。
ApplicationController
にincludeされるということで、全てのControllerから呼び出すことができるようになります。
認証用モデルごとのhelper系メソッドの作成
curret_user
やauthenticate_user!
のような、Deviseを使ったことがある方にはおなじみのメソッドを作ってくれます。
これらのメソッドは認証用のモデルごとに作られますので、例えばUser
モデルとCustomer
モデルを認証用に使っている場合は、current_user
とcurrent_customer
が、それぞれ作られます。
作られる場所は、ApplicationController
にincludeされる、Devise::Controllers::Helpers
というModuleに作成されます。
したがって、全てのControllerから呼び出すことができるようになります。
これらのメソッドはroutes.rbに記述するdevise_for
の中で作られます。
文字通り動的にメソッドを作るので、多くのIDEでは「Jump to Definition」でこれらのメソッドにJumpできない状態になってしまっています。
メソッドを作っているコードは こちら から見れますので、気になる方はぜひみて見てください。
作ってくれるメソッドは次の4つです。(認証用のモデルとしてUser
が設定されている想定)
メソッド名 | 概要 |
---|---|
authenticate_user! | 認証されているかどうかを確認し、認証されていない場合は認証失敗時のアクションを行う(例:トップページにリダイレクトする) |
user_signed_in? | 認証されているかどうか |
current_user | 認証されているモデルのインスタンスを取得する |
user_session | 認証用モデルのセッション情報にアクセスする |
また、authenticate_user!
以外のメソッドはViewからも呼び出せるようにしてくれています。
(内部でControllerの helper_method を呼び出している)
モジュールごとに必要な仕事
モジュールとは、ユーザー認証に関連する「機能」のまとまりのことです。
認証用モデルごとに「どのモジュールを使うか」を設定できます。
設定はモデルのdevise
メソッドで行えます。
デフォルトでは次のように、5つのモジュールを使う形で設定されます。
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
end
モジュールごとに必要な仕事は、「モジュールの機能」ごとに変わってきますが、大まかに分けますと、「Routesを定義する」部分と「モデルを拡張する」部分に分けられます。
具体的な仕事内容を把握するために、2つのモジュールについて説明します。
Database Authenticatable モジュールの仕事
Database Authenticatableは、「DBに保存されたパスワードを使ってユーザーを認証する機能」のモジュールです。
この機能を実現するために必要な下記のRoutesが定義されます。
- ログイン画面 [GET] /users/sign_in to devise/session#new
- ログイン処理 [POST] /users/sign_in to devise/session#create
- ログアウト処理 [DELETE] /users/sign_out to devise/session#destroy
Routesに対応するアクションの実装は(ControllerとView)gem内の appディレクトリ に用意されています。
gem内のappディレクトリ以下のアクションの実装を、Routesに設定できる仕組みについて知りたい方は Rails エンジン入門 を見ていただければと思います。
モデルは次のように拡張されます。
- ユーザーを照合するための
encrypted_password
とemail
を必須のカラムとする - パスワード認証に必要な各種メソッドを追加する
- password=(new_password): パスワードをセットする
- valid_password?(password): パスワードが正しいかどうかをチェックする
他にもたくさんメソッドが追加されますので、詳しく知りたい方は rubydocのドキュメント をご覧ください。
Validatable モジュールの仕事
Validatableは、emailとpasswordの「入力の有無」や「形式のチェック」などのValidationをしてくれるモジュールです。
Routesはなく、モデルの拡張も最小限です。
主な仕事は、モデルのValidation用のvalidates_presence_of
メソッドやvalidates_format_of
を使って、emailとpasswordに対してValidationの設定を行うことです。
このように「モジュールの機能」によって必要な仕事は変わりますが、他のモジュールも大体このようなことをやっています。
他のモジュールについては、DeviseのREADME の冒頭にモジュールごとにrubydocへのリンクが貼られており、そちらに詳細が書かれています。
以上、Deviseがやってくれる仕事を具体的に把握することで、モヤモヤの原因の1つである「全容を掴みづらい」を解消することを試みてみました。
10個もモジュールがあるので完璧に把握することは難しいですが、次のように認識しておけば、だいぶスッキリするのではないでしょうか。
- 基盤部分とモジュールごとの部分を分けて捉える
- 基盤部分の仕事は、主にApplicationControllerにユーザー認証に必要な基本的なメソッドを付与すること
- モジュールごとの仕事は、主に「Routesの定義」と「モデルの拡張」
- モデルの拡張について詳細を知りたい場合はrubydocを見る
Deviseの仕事の全容が掴めたところで、次は認証処理の内側を把握する事で「ブラックボックス感の解消」を試みたいと思います。
認証周りの処理の内側を把握する
Deviseの認証周りの処理の内側を見ていきます。
認証周りの処理とは、具体的に言いますと、「ログイン処理」、「ログアウト処理」、「ログインしているユーザー情報の取得処理」です。
「ログイン処理」は少しややこしいので後回しにして、まずは「ログアウト処理」と、「ログインしているユーザー情報の取得処理」を見てみましょう。
ログアウト処理
ログアウト処理はDevise::Controller::SignInOut
に定義されているsign_out
メソッドで行います。
Devise::Controller::SignInOut
はApplicationController
にincludeされるので、全てのControllerから呼び出すことができます。
sign_outメソッドの使用イメージ:
sign_out(:user)
sign_out
メソッドのコードを貼り付けてみます。
しっかりと読む必要はないですが、ざっと見でもいいので、雰囲気を感じてもらった方良いかなと思い。
lib/devise/controller/sign_in_out.rb から一部抜粋:
# Sign out a given user or scope. This helper is useful for signing out a user
# after deleting accounts. Returns true if there was a logout and false if there
# is no user logged in on the referred scope
#
# Examples:
#
# sign_out :user # sign_out(scope)
# sign_out @user # sign_out(resource)
#
def sign_out(resource_or_scope = nil)
return sign_out_all_scopes unless resource_or_scope
scope = Devise::Mapping.find_scope!(resource_or_scope)
user = warden.user(scope: scope, run_callbacks: false) # If there is no user
warden.logout(scope)
warden.clear_strategies_cache!(scope: scope)
instance_variable_set(:"@current_#{scope}", nil)
!!user
end
7行のメソッドですが、実際のログアウト処理は、真ん中の warden.logout(scope)
の部分です。
scope
とは、認証用のモデルをシンボルで表したもので、たとえばUser
モデルの場合は:user
が入ります。
Deviseでは複数のモデルを認証用に使えるので、各処理において、どのモデルへの処理なのかを識別する必要があります。
ということで、ログアウト処理はwarden
というものに委譲していることが解りました。
Wardenとは、ユーザー認証機能を使えるようにしてくれるRack Middlewareです。
https://github.com/wardencommunity/warden
DeviseのREADMEの先頭の文にも「DeviseはWardenをベースにしている」と書かれています。
DeviseのREADME から一部抜粋:
Devise is a flexible authentication solution for Rails based on Warden
まず、Rack Middlewareについて、一言でざっくり言いますと、「Railsがrequestを処理する前段階で、ひと仕事するもの」という感じです。
Wardenの場合の「ひと仕事」にあたるのが、「ユーザー認証機能を使えるようにする」です。
Rack Middlewareについてちゃんと理解したい方は、この記事が解りやすいと思います。
https://qiita.com/nishio-dens/items/e293f15856d849d3862b
ということで、ログアウト処理の内側を知るためには、Wardenのlogout
メソッドを見てみる必要がありそうです。
Wardenのログアウト処理
Wardenのログアウト処理は、Warden::Proxy
クラスのlogout
メソッドに実装されています。
ログアウト処理なので、主な処理内容は「認証したユーザー情報の削除」です。
lib/warden/proxy.rb から一部抜粋:
def logout(*scopes)
# ...
scopes.each do |scope|
user = @users.delete(scope)
# ...
session_serializer.delete(scope, user)
end
# ...
end
こちらにもscope
というものがありますね。
先ほど説明したように、認証用モデルをシンボル化したもので、Userモデルの場合は:user
となる形でした。
*scopes
と可変長引数になっているので、logout(:user)
や logout(:user, :customer)
のような指定が可能になっています。
Deviseが複数のモデルを認証用に使えるのは、Wardenのscopeの概念をそのまま活用している、ということになります。
https://github.com/wardencommunity/warden/wiki/Scopes
logoutメソッドのコードを見てみると、scopeごとに2つのデータを削除しています。
⑴. usersインスタンス変数から削除
@users.delete(scope)
usersインスタンス変数はscopeごとの「認証されたユーザー情報」を保持する変数です。
Deviseのcurrent_user
やuser_signin?
のメソッドは、この値を参照するので、削除する必要があります。
⑵. Sessionから削除
session_serializer.delete(scope, user)
Sessionというワードが出てきたので、ざっと説明してみたいと思います。
既に知っている方は、以下2つの段落は読み飛ばしていただいて構いません。
Sessionとは、アクセスをまたいでデータを保持する仕組みです。
ユーザー認証機能で考えた場合、一度ログインしたら、それ以降のアクセスも「ログインされた状態」を引き継ぐ必要があるので、この仕組みが必要になります。
データを保持する場所を一般的にSession Storageと呼び、RedisやDatabaseやCookieなどをSession Storageとして活用できます。
Session Storageは、サイトにアクセスされた全てのユーザーのSessionデータを保持しています。
したがって、Session Storageの中から、アクセスしてきたユーザーのSessionデータを判別する必要があり、そのためにSession IDというものを使います。
Session IDはユニーク(一意)な文字列でして、最初のアクセスの際にサーバーが生成します。
生成したSession IDをCookieを通してブラウザに送り、また、ブラウザーからサーバーに送られます。
このSession IDのおかげで、サーバーはアクセスをまたいでユーザーを識別することが可能になります。
ここまで、Sessionについてざっくりと説明してみました。
SessionやCookieについてはWEB上に色々わかりやすい記事があるので、もっと詳しく知りたい方は検索してみると良いと思います。
SessionもRack Middlewareとして実装されていて、Wardenの前段階で、Sessionデータを操作できるようにしてくれます。
具体的には、CookieのSession IDを元に、Session Storageの中から対象ユーザーのSessionデータを特定して、読み書きできるように準備してくれる感じです。
Sessionの読み書きは次のような形で行えます。
env["rack.session"]["foo"] = "abc" # key:fooに対して"abc"を書き込む
env["rack.session"]["foo"] # key:fooの値を読み取る
env["rack.session"].delete("foo") # key:fooを削除
Rack Middlewareは、envを通してやり取りするので、こういった形になっています。
ちなみに、DeviseからWardenを使うのも env["warden"].logout(:user)
といった形になります。
少し脱線してしまいました。
Wardenのlogoutメソッドの下記のコードで、セッションから「認証されたユーザー情報」を削除しているという話でした。
session_serializer.delete(scope, user)
Sessionデータの「認証されたユーザー情報」のKeyはwarden.user.{scope}.key
という形式です。
したがって、上記のコードはscopeとして:user
が指定された場合、内部で下記のようなコードを実行します。
env["rack.session"].delete("warden.user.user.key")
登場人物が多くて少し複雑なので、ログアウト処理の全体の流れを図で表してみました。
以上、ログアウト処理の内側をのぞいてみました。
少々複雑な構成になっていますが、登場人物とSessionでのデータの持ち方を把握できたことで、だいぶブラックボックス感が薄れてきたのではないでしょうか?
ログインしているユーザー情報の取得処理
続いて、「ログインしているユーザー情報の取得処理」の内側を見ていきます。
ログアウト処理の説明で、WardenやSessionについて触れたので、こちらはサクッといきます。
「ログインしているユーザー情報の取得処理」は、Deviseのメソッドで言いますとcurrent_user
やuser_sign_in?
です。
これらのメソッドは、認証用のモデルごとに動的に生成されているメソッドでした。
メソッドを生成している部分のコードを見てみます。
lib/devise/controller/helper.rb から一部抜粋:
def #{mapping}_signed_in?
!!current_#{mapping}
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
#{mapping}
の部分は、認証用のモデルごとに動的に変わる部分でして、Userモデルの場合はuser
が入ります。
Userモデルの場合に生成されるメソッドのコード:
def user_signed_in?
!!current_user
end
def current_user
@current_user ||= warden.authenticate(scope: :user)
end
user_sign_in?
メソッドはcurrent_user
の結果をBoolに変換しているだけですね。
current_user
メソッドもたった1行で、Wardenのauthenticate
メソッドを呼び出して、結果をcurrent_userインスタンス変数に格納しています。
インスタンス変数に格納することで、2回目以降のcurrent_user
へのアクセスでは、直接インスタンス変数を返すことになるので、warden.authenticate
の呼び出しが省略されます。
ということで、ログアウト処理と同様に、主な処理はWardenが行なっています。
Wardenのauthenticate
メソッドの内容を見ていきます。
コードが少し入り組んでいるので、こちらには記載しませんが、やっていることは主に次の2つです。
- Sessionに認証されたユーザー情報があれば、それを返す
- Strategyを元にユーザー認証処理を実行する
⑵のStrategyについては、次のログイン処理の部分で説明しますので、ここでは触れません。
⑴の「Sessionから認証されたユーザー情報を取得する」ですが、Sessionデータに関しての基本的な部分は「ログアウト処理」のところで説明したので省略します。
ここでは、「ユーザー情報をSessionデータに変換する処理」と「Sessionデータからユーザー情報に復元する処理」について触れてみたいと思います。
Deviseにとって「ユーザー情報」は「モデルのインスタンス」です。
Sessionに保持するデータは「モデルのID」です。
したがって、Sessionにユーザー情報を格納するとき「モデルのインスタンスをIDに変換」して、Sessionからユーザー情報を取得するとき「IDからモデルのインスタンスに変換」する処理が必要になります。
Wardenにそれぞれの処理を設定するオプションが用意されていて、Deviseがこの設定をしています。
- Sessionへユーザー情報を格納する処理の設定:
serialize_into_session
- Sessionからユーザー情報を読み取る処理の設定:
serialize_from_session
設定している場所は下記ですが、かなり抽象化されているので解りにくいかもしれません。
lib/devise.rb
細かい処理の内容はまで把握する必要はないと思いますが、WardenとDeviseの関係性は理解しておくと良いと思います。
Wardenは、モデルやRailsの存在を知らずに、抽象的な形の「ユーザー情報」のみを扱っていて、具体的な処理はオプションとして設定できるようにしてくれています。
Deviseは、Wardenが提供している「ユーザー認証機能」をRailsから活用できるようにしてくれています。
Deviseは、RailsとWardenの橋渡し役をしてくれていると表現できるかもしません。
ログイン処理
最後にログイン処理を見てみましょう。
ログイン処理として、ユーザーを認証する方法は2つあります。
-
sign_in
メソッドに直接ユーザー情報を渡す - Wardenの
authenticate
メソッド経由でStrategyを実行する
それぞれ説明していきます。
sing_inメソッドに直接ユーザー情報を渡す
このメソッドは前述した「Deviseの全容を掴む」の中の「ユーザー認証の基本的なメソッドの付与」で、ApplicationControllerに付与される認証用のメソッドとして紹介したものです。
したがって、全てのControllerから、次のように呼び出すことができます。
sign_in(user)
Omniauth でソーシャルログインを実装する際のcallbackのアクションでは、こちらのメソッドを使って認証処理を行う場合が多いと思います。
このメソッドもやはり、主要な処理はWardenに委譲する形で、Wardenのset_user
メソッドを呼び出しています。
Wardenのset_user
メソッドでは次の2つの処理をしています。
-
users
変数にユーザー情報を格納 - Sessionにユーザー情報を格納
当然ながら、ログアウト処理と逆のことをやっていますね。
users変数やSessionデータに関しては既に説明したので、ここでは説明を省略して、2つめのログイン処理の方法の説明に移ります。
Wardenのauthenticate
メソッド経由でStrategyを実行する
Database Authenticatableモジュールのログイン処理のアクション "devise/session#create" でこの方法を使っています。
app/controllers/devise/session_controller.rb から一部抜粋
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
# ...
respond_with resource, location: after_sign_in_path_for(resource)
end
Wardenのauthenticate!
メソッドでは、既にユーザー認証済みの場合はそのユーザー情報を返し、認証されていない場合はStrategyを実行することで、ユーザー認証を試みます。
Strategyとは、requestのデータを元にユーザー認証を行う仕組みです。
https://github.com/wardencommunity/warden/wiki/Strategies
Strategyは次の2つのメソッドを実装する必要があります。
-
valid?
: requestのデータを元にStrategyを実行するかどうかを判断する -
authenticate!
: requestのデータを元にユーザーを認証する
これらのメソッドを定義したクラスを、予めWardenのstrategiesに設定しておくと、authenticate!
メソッドが呼ばれたときに、自動的に呼び出してくれるようになります。
Deviseがこの仕組みを、Database AuthenticatableモジュールとRebemerableモジュールのユーザー認証に使っています。
たとえば、Database Authenticatableモジュールの場合は次のような実装をしています。
valid?
メソッド:
- 対象の認証モデルがパスワードによる認証を許可しているか
- 対象のアクションがパスワードによる認証を許可しているか
- requestのデータに認証のパラメーターが設定されているか(デフォルトでは
email
とpassword
)
authenticate!
メソッド:
-
email
とpassword
から対象のユーザーを探す
Strategyの仕組みを活用することで、ログイン処理のアクションの実装から、ユーザー認証の処理を切り離すことができます。
前述した、"devise/session#create" アクションの実装も、「requestのパラメーターのチェック」や、「モデルからレコードをfindする処理」もなく、ただ warden.authenticate!
を呼び出しているだけでした。
一方で、全体の構造としては複雑になってしまっているので、一概にStrategyが「良い仕組みである」と評価することは難しいと思いました。
「Strategyという仕組みを使っている理由」を明確にして、綺麗に締めることができればと思いましたが、このように設計に関することを考察することは難しいですね。
「Strategyという仕組みを使っている」ということを知っているだけでも、ブラックスボックス感を柔らげてくれると思うので、まあこのレベルの掘り下げ度で、記事の目的としては良いのかなと思いました。
終わりに
Deviseを使っていて感じるモヤモヤの原因を 「全容を掴みづらい」,「ブラックボックス感がある」 と定めて、それぞれを解消することを試みてみました。
この記事を読んでいただいたことで、皆さんのモヤモヤを少しでも解消できていましたら嬉しく思います。
ところで、Deviseに関して色々とネガティブな意見を聞くことがあります。
私も漠然とそういった気持ちを持ってしまっていました。
ネガティブな気持ちを掘り下げると、
- 今回のようなモヤモヤによるもの
- 設計や思想に関するもの(例: モデルに色々入れ込むことは良い設計ではない)
があると思います。
モヤモヤによるものは、主に勉強不足からくるものだと思うので、毎度掘り下げるのは現実的に大変ですが、OSSとして使わせていただいている立場として、謙虚でいたいなと思いました。
設計に関するものは、取り組んでいるプロジェクトの問題領域や開発体制などに絡んで、色々な意見があるように思います。
今回見てきたように、Rack Middlewareとして色々なパーツも使えるので、プロジェクトに応じて自分で認証機能を作るのも1つの現実的な方法だと思います。
設計に関して考察する場合は、今年(2020年)話題になった、パーフェクトRails著者が解説するdeviseの現代的なユーザー認証のモデル構成について という記事が参考になると思います。
以上、最後まで読んでいただいてありがとうございました!
モヤモヤを解消することで、少しでも快適なRailsライフを送る手助けができましたら嬉しく思います!