QA@IT

ソーシャルログインと普通のログインを併用する際の設計

7702 PV

現在、Rails3.2とDevise,Omniauthというログイン用のGemをつかって開発をしているのですが、プログラムの設計で悩んでいます。

具体的に何がしたいかというと、
ブログサービスを開発しているのですが、このサービスにはログイン方法を二種類用意しようと考えています。

一つがTwitter,FaceBook等を使ったOauthでのログイン。
もう一つが一般的な、メールアドレスとパスワードを入力してもらうログイン。

この機能を実装するにあたり、どのようにModelを設計すればよいかわからなくなってきました。

  • 私の設計

まず私が考えた設計なのですが、

OmniUser(Oauthでのログイン用)、User(メールアドレスでのログイン用)の2つのModelとDBを設計しました。

OmniUserはuidとproviderというカラムを持っており、この2つが一致した場合にログインできるようにします。
Userはemailとpasswordというカラムを持っており、この2つが一致した場合にログインできます。

  • 問題点

この私の考えた設計の問題点なんですが、

例えばブログというModelを作成する際に、ユーザとブログで1対多関係を結ぶのが自然と思われます。
そこで、ブログModelはカラムにuser_idみたいなものを持つべきなのですが、このuser_idとはOmniUserなのかUserなのかわからないため、どちらのテーブルを見に行ったらいいかわかりません。
そのためどうやって、この処理を書くかという問題点です。

他の問題点としては、
ログインはOmniUser、Userどちらか片方のみがログインしているべきであり、両方がログインしている状態があってはいけません。
このあたりの制御をどう書くかという問題です。

どちらの問題も頑張れば書けるのですが、設計の段階でまずい物ではこのあたりの処理がかなり複雑になってしまうのではないかと恐れています。

  • 質問

そこで改めて質問なのですが、この様な場合、どのような設計をすればよいでしょうか?ご意見をお聞かせいただけると幸いです。

回答

卓袱台返しになってしまいそうですが、「普通のログイン」もOmniAuthに任せる(omniauth-identity)という選択肢はないのでしょうか?

編集 履歴 (0)
  • omniauth-identityというGemがあったのですね。しりませんでしえた。

    ただ、この方法はテーブルを別々に2つ作るという形ですよね(勘違いしてたらすみません)?
    この形ですと「問題点」の部分の解決が大変そうなので、今回は使わない方向でいこうと思います。
    ご回答ありがとうございました。
    -

railsの場合はモデルの単一テーブル継承または多態化を利用するのがいいのではないかと思います。

http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance

http://guides.rubyonrails.org/association_basics.html#polymorphic-associations

今回はSTI(Single Table Inheritance)の方でいいように思いますね。

UserBase モデル
User モデル
OmniUser モデル
の3つで設計する場合を考えると、
必要な項目はすべてUserBaseモデルに格納します。
まずは基本形として元々 uidとemail, providerとpasswordに分かれていたものを同じ列にしてみて考えてみます。
ここでは uidとauth_valueとしました。
モデルは以下のようになります。

class UserBase < ActiveRecord::Base
  accessible_attr: uid, auth_value
end

class User < UserBase
end

class OmniUser < UserBase
end

ただしUserBasesテーブルにはtype列を追加する必要があります。
(今回動作確認にあたってはrails g model UserBase uid:string auth_value:string type:stringで作ってモデルのaccessible_attr:からtypeを削除しちゃいました。)

こうすると

> u = UserBase.new
> u.uid = "uid"
> u.save

> u = User.new
> u.uid = "uid"
> u.save

> u = OmniUser.new
> u.uid = "uid"
> u.save

というように保存することができます。
取得の方はUserBase.allですべて抽出できますのでブログmodelとはUserBaseを関連づけておけばいいのではないでしょうか。

STIを利用したほかのバリエーションとして、

  • UserBaseに必要な列をすべて用意しておく
    UserモデルとOmniUserモデルで項目名を分けたい場合はUserBaseにすべての列を用意しておき、
    Userモデルは email,passwordで、OmniUserはuid,providerで利用することもできます(使われてない列が空になるだけです)。
    ただ、今回のケースならUserモデルとOmniUserモデルにアクセサメソッドがあればいいだけのように思います。
    同様にどちらかにだけ必要な項目もこのように定義することができます。

  • Userとそれを継承したOmniUserの2つだけで実装する
    テンプレ的に親と継承する子2つでサンプルを書きましたが、UserBaseを用意する必要がないなら 3つではなく、
    User < ActiveRecords::BaseOmniUser < Userの2つだけで実装したほうがいいかもしれません(この場合UserのTypeはnilになるようです)。

UserBase.allで取得しても各アイテムはinstance_of?で判別可能です。
(先にあげた保存の例の3件しか入っていない場合)

> UserBase.all[2].instance_of? UserBase
=> false
> UserBase.all[2].instance_of? OmniUser
=> true

なるべく判別しなくても使える(使う側は実体がどちらか意識する必要がない)ようになっていたほうが綺麗ですけどね。

編集 履歴 (2)
  • お返事ありがとうございます。

    STIという考え方があったのですね。これなら一つのテーブルにデータを集約する形になるので、管理しやすそうですね。
    Gemとの折り合いはある程度つけないといけなさそうですが、そこは頑張ればなんとかなりです。
    この方法でいってみようと思います。

    ありがとうございました。
    -

以前利用した場合はような以下のような設計にしました。

Blog *- User -* OmniUser

User から Blog は 1対多で、 User から OmniUser が1対多です。

Oauth でしか認証しない場合でも User のインスタンスを作成します。
メールアドレスの登録を不要にしたい場合はまあ無理矢理なんとかするとして、パスワードはランダムな値に設定しておきます。
Oauth するサービスは複数あるので User に対して複数あるはずなのでこのような構成になりました。
「Twitterだけ!」とかになれば User ひとつにしたりすると思います。

ironsand さんがコメントで捕捉していただきました。
social-login-in-rails というサンプルプロジェクトが似たような設計になっているそうです。oauth の情報は Authorization というモデルにあります。

結びつく User が存在しない場合はoauthの情報を利用したりして作成していました。

Doorkeeper が Oauth で認証した場合でもメールアドレスが必須なので、似たような設計なのかと勝手に思っています。

編集 履歴 (1)
  • eielさんの補足になりますが、同じことをしてるサンプルのプロジェクトがこちらにありました。 モデル名は OmniUserではなくAuthorizationを使ってるようです。 https://github.com/mohitjain/social-login-in-rails -
  • 素晴しい捕捉ありがとうございますー。コメントだとリンクになってないようなので、追記させていただきました。 -
  • 「Oauth するサービスは複数ある」この部分を恥ずかしながら想定できていませんでした。たしかに複数ある場合は、1対多関係にする必要性がありますね。ただ書いてないのですが今回のログイン機能を実装する目的として、「ワンボタンでユーザ登録ができる」「ソーシャルアカウントを持ってない人もサポートする」がありまして、その点から考えるとSTIの方シンプルで良いと考えました。でも今後の参考にさせていただきます -
ウォッチ

この質問への回答やコメントをメールでお知らせします。