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/initializers/devise.rb

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が保存できなかった。

CustomerUser.rb

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別に処理を作っても、共通で作ってもよい。

omniauth_callbacks_controller.rb

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を追加する

application_controller.rb

after_filter :prepare_unobtrusive_flash

CSSを呼ぶ

application.css

*= require pnotify

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

flash.js

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

JSも呼ぶ

application.js

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

Profile picture

ぴーやま
プログラミングを嗜んでします。中華料理で出てくるたまごふわふわのコーンスープが好きです。