私は普段 Railsエンジニアとして働いています。Twitterクローンといえば、TechXXXみたいなプログラミングスクールの最終課題となっていることも多く、ポートフォリオとして日々量産されているアプリケーションの一つだと思います。メンターのアルバイト経験のある方は「入力と出力が出来るようになれば、なんだって作れるから」と、挫折しかけている受講生の方を励ましたりしたこともあるかと思います。
そんな見飽きたTwitterクローンですが、プロのエンジニアである私もポートフォリオのために(?)本気で作ってみました。それが以下のサイトです。
まあ基本はマイクロポスト出来て、いいね、フォローが出来るという普通の Twitter クローンです。Markdown で書けるとか絵文字でリアクション出来るとか多少工夫したところがありますので、気軽に使ってみていただけると嬉しいです。最終的には Wiki と融合した情報サイトを目指しています。
で、個人でサイトを開発して最も大変だったのは「認証」の部分です。本気なので妥協した設計は入れません。それを今から解説していきます。認証は gem に頼らず rails8 の機能のみで作成しています。
Devise などの既存の認証システムの問題点
仕事では Devise gem を使っていますが、Devise や Sorcery、Rails チュートリアルで作成するユーザー(=認証)モデルは、大きな問題を抱えていると考えています。
以下の図は、User モデル と Post などの User に関連付けられるコンテンツとの関係を、ざっくりと表したものです。
User モデルには、認証で使う email, password の他に、コンテンツの表示に使う nickname、サインアップなどの進捗を管理する token, confirmed_at などのカラムがあります。さらにメールアドレスの変更を行う機能があると unconfirmed_email カラムが追加されたり、パスワードロックを行うための locked_at が追加されたりします。
で、User モデルはSNSの基本となるモデルですので、ウェブサービスに機能が追加されるたびに has_many なんちゃらみたいなのがどんどん増えていき、30キロバイトくらいの巨大な user.rb が出来あがることになります。
要は User モデルにすべてを詰め込め過ぎ、モデルの責務が多くなりすぎになりがちです。また、User モデルが「未認証」のような「状態(status)」を持つため、テストや実装が大変になります。
Freespk で実装した認証システム
これに対し、私が採用した方針は「とにかくモデルを分割する」というものです。Concerns ではありません。テーブルごと分割します。その関係をざっくりと表したものが以下の図になります。
それぞれのモデルがどう動くのかをユーザー登録を例に説明します。
- 利用者が「ユーザー登録」を行おうとすると Signup レコードと Confirmation レコードが作られます
- Confirmation モデルが作られると、利用者に確認メールが送信されます
- token を含んだURLをクリックすると、Confirmation と Signup が confirmed 状態になります
- Signup が confirmed 状態になると、Authentication レコードと User レコードが作成されます
User は基本的にはコンテンツしか管理しません。has_many の集合体のようなモデルとなります。また、Userレコードには「登録したけど、まだ未認証」みたいな中途半端な status は無いので、せいぜい退会したかどうかだけを管理すれば良くなります。
ちなみに、Signup の部分には PasswordReset や EmailChange などの似た責務を持つモデルが存在します。それらの手続きが完了した場合に、Authentication のカラムを変更するようになっています。
モデルを分割したことによる利点
ひとつひとつのモデルが小さくなり責務は一つだけなので、修正やテストが楽になります。たとえば単体テストにおいては User レコードと Post レコードを作るだけでOKです。Authenticationレコードを作成したり「confirmed_at がセットされているか否か」を気にする必要はありません。
モデルを分割したことによる欠点
User モデルと認証にかかわるモデルの間は「疎結合」になっていますが、認証部分は多数のモデルが連携(と言っても一度に関連するのは3つ程度)して、主に callback で状態の変更を伝え合うことになります。たとえば、Confirmation が confirm されたら、次に Signup を confirm して、さらに Authentication を create し、成功したら after_validation で User を build して一緒に save するといったような感じです。
Rails に慣れない方は Service モデルみたいなのを入れたくなるかもしれません(入れてはダメです)。
Devise 的な設計には他にも問題が...
確認メールのURLをクリックしたら、たとえ誤クリックでも認証されてしまうとか、2要素認証をまともに動かすのは難しいとか、魔改造に伴うバッドノウハウが各社それぞれに蓄積されているような気がします。
この記事をきっかけに、認証システムの簡素化について思いを巡らせていただけると幸いです。