Rails4 + Devise + OmniAuthで複数のログイン機構を作る

やりたいこと

  • adminとuserでmodelを分けて、別々のログイン機構を持ちたい。
  • adminは通常のdeviseで、emailとpasswordを使用したログイン。
  • userはdevise+Omniauthで、Twitter, Facebook, Githubのアカウントを利用してログイン。
    初回アクセス時、各サービスでOAuthの認証を受けてcallbackが呼ばれた際に渡されたauth情報を元に、別途emailやpasswordを持たずにユーザー登録。
  • まだ検証中なので実装のための下調べをおおまかにメモった感じなので実装の場合は気をつける。

    材料

    ・Rails(4)
    ・Devise
    ・Omniauth
    ・Omniauth-twitter
    ・Omniauth-facebook

    Deviseのインストールと各種部品の作成

    devise:installをして、管理対象となるユーザーのVMCを作成

    rails g devise:install
    rails g devise CustomerUser
    rails g devise:controllers customer
    rails g devise:views customer
    rails g devise AdminUser
    rails g devise:controllers admin
    rails g devise:views admin

    routesでパスを分ける

    routesでadmin/sign_inとcustomer/sign_inといった感じに処理を分ける

    Rails.application.routes.draw do
    
    	namespace :admin do
    		devise_for :admin_users, path: '',
    		:path_names => {:sign_in => 'login', :sign_out => 'logout', :sign_up => 'register'}, 
    		:controllers => {
    			:sessions      => "admin/sessions", 
    			:registrations => "admin/registrations",
    			:passwords     => "admin/passwords"
    		}
    	end
    
    	namespace :customer do
    		devise_for :customers, :path => '', 
    		:path_names => {:sign_in => 'login', :sign_out => 'logout', :sign_up => 'register'}, 
    		:controllers => {
    			:sessions      => "customer/sessions", 
    			:registrations => "customer/registrations",
    			:passwords     => "customer/passwords",
    			:omniauth_callbacks => "customer/omniauth_callbacks" 
    		}
    	end
    	root 'top#index'
    	
    end

    ここでscope長い問題

    namespace :adminの以下に:admin_usersってやった余波でscopeが冗長に。
    user_signed_in?も、「admin_admin_user_signed_in?」みたいになっちゃって長い。
    設計ミスだなーどうしようかなーと思いつつ検証なのでそのまま進める。
    本当はnamespace以下じゃなくてdevise_forのpathにadminと記述して
    他のadmin系処理だけnamespace以下に記述すればいいんだねって気づいた時にはすでにおすしなのであった。

    deviseのconfigを設定する

    deviseのconfig設定で、scoped_viewsをtrueに設定し
    omniauth用のAPP IDなどを設定しておく。KEYの類は環境変数に。
    local開発時はdotenv-railsというgemで環境変数を管理すると便利ねぇ。

    config.scoped_views = true
    config.omniauth :twitter, ENV['TW_API_KEY'], ENV['TW_SECRET_KEY']
    config.omniauth :facebook, ENV['FB_APP_ID'], ENV['FB_SECRET_KEY']
    

    modelの設定

    FBだけfirst_or_createの書き方だとうまくusernameが保存できなかった。

    class CustomerUser < ActiveRecord::Base
      devise :database_authenticatable, :registerable,
             :validatable, :omniauthable, :omniauth_providers => [:twitter, :facebook]
      validates :uid,
        uniqueness: {
          message: "このアカウントは既に登録済みです",
          scope: [:provider]
        }
    
      def self.find_for_twitter_oauth(auth)
        where(provider: auth.provider, uid: auth.uid).first_or_create! do |user|
          user.username = auth.info.nickname
        end
      end
    
      def self.find_for_facebook_oauth(auth)    
       user = CustomerUser.where(:provider => auth.provider, :uid => auth.uid).first
        unless user
          user = CustomerUser.create(
            username: auth.extra.raw_info.name,
            provider: auth.provider,
            #token: auth.credentials.token,
            uid: auth.uid
            
          )
        end
        user
      end
    
      def self.new_with_session(params, session)
        if session["devise.user_attributes"]
          new(session["devise.user_attributes"], without_protection: true) do |user|
            user.attributes = params
            user.valid?
          end
        else
          super
        end
      end
      # providerが入っている場合
      def password_required?
        super && provider.blank?
      end
    
      def email_required?
        false
      end
    
      def update_with_password(params, *options)
        if encrypted_password.blank? 
          update_attributes(params, *options) 
        else
          super
        end
      end
    
    end
    

    omniauth_callbacks_controllerを作る

    devise:controllersで入れ物は作ってくれているので、中に処理を追加する。
    それぞれのSNS別に処理を作っても、共通で作ってもよい。

    def twitter
        @user = CustomerUser.find_for_twitter_oauth(request.env["omniauth.auth"])
        if @user.persisted?
          flash.notice = "ログインしました"
        else
          flash.notice = "ログインできませんでした"
        end
        sign_in_and_redirect @user, :event => :authentication
    end

    Viewにログインのリンクを設置

    <a href="<%= customer_user_omniauth_authorize_path(:twitter) %>">Twitter Login</a>
    <a href="<%= customer_user_omniauth_authorize_path(:facebook) %>">Facebook Login</a>

    せっかくなのでPNotifyでログインしたよメッセージを出す

    https://github.com/sciactive/pnotify/wiki/Rails-flash-integration-with-PNotify
    Gemfileに追加してbundle installしておく

    gem 'rails-assets-pnotify'
    gem 'unobtrusive_flash', '>=3'

    application_controllerにfilterを追加する

    after_filter :prepare_unobtrusive_flash

    CSSを呼ぶ

     *= require pnotify

    flash.jsを作る
    このへんは通常のPNotifyと同じなのでPNotifyの説明を見ながらいい感じにする。

    $(document).ready(function() {
      $(window).bind('rails:flash', function(e, params) {
        new PNotify({
          title: params.type,
          text: params.message,
          type: params.type,
          icon: false
        });
      });
    });

    JSも呼ぶ

    //= require pnotify
    //= require unobtrusive_flash
    //= require flash