- ユーザー登録において、メールアドレス認証の他に、Twitter、Facebook、googleなどのSNS認証機能を付けたい。
- ライブラリ(Gem)のOmniAuthを使用( SNSのAPIにリクエストを送ると、API上で認証後、SNSの登録情報がアプリ側に返される流れ)。
- 参: OmniAuth のGithub
- 参: google-oauth2 のGit Hub
実装する機能
- メールアドレスでのユーザー登録。
- SNS認証でのユーザー登録(名前とメールアドレスを自動入力、PW不要。その他の情報は入力させる)。
- メールアドレス登録済みなら、SNSを紐付ける
手順
1. API側の設定 〜 アプリ側の基本設定
- 各サイトでAPI登録し、環境変数で、omniauthを設定。
- Qiita: Twitter、Facebook、googleのAPI登録までの手順
2. アプリ側の実装(メソッドの設定)
2-0. (事前準備)メールアドレスでの登録機能の実装
- 追加したカラムを受け取るためのストロングパラメータ設定。
application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters # メールアドレス以外の自分で追加したカラムを許可
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, ..])
end
- 仮の確認用のビューを作成.
users/index.html.haml
- if user_signed_in?
= "#{current_user.name}でログイン中"
= link_to 'ログアウト', destroy_user_session_path, method: :delete
- else
= link_to '新規登録', new_user_path
= link_to 'ログイン', new_user_session_path
users/new.html.haml
= link_to 'メールアドレスで登録', new_user_registration_path
ここで、動作確認しとく。
2-1. SNS認証(OmniAuth)の実装
- 今回は、SNS認証と、Userモデルに保存するタイミングが異なる!
- SNS情報から、取得したユーザー情報を照合(今回は、クラスメソッドで切り出して実装)。
- SNS照合結果から、アプリ登録済みユーザーか? → ログイン or 新規登録画面へ遷移。
- 公式ドキュメントには、「登録ボタンクリック → ユーザー登録完了」の仕様が記載。
コントローラー
- twitter、facebook、google_oauth2アクションから処理(コールバック関数名:authorization)を呼ぶ。
- SNS認証時に呼ばれるコールバック関数名は決まってる。
- APIからのレスポンスが
request.env["omniauth.auth"]
変数に入る。 - DB操作を行うメソッド
User.from_omniauth
を作り、変数を渡す。- このメソッドで、Userモデルのインスタンスを返してもらって、@userに代入しとくことで、deviseのヘルパーが使えるようになる。
- @userが未保存(新規登録)の場合、ビューに@user変数を渡したいので、renderを使用。
users/omniauth_callbacks_controller.rb
def twitter
authorization # コールバック
end
def facebook
authorization # コールバック
end
def google_oauth2
authorization # コールバック
end
def failure
redirect_to root_path
end
private
# コールバックのメソッド定義
def authorization # APIから取得したユーザー情報はrequest.env["omniauth.auth"]に格納されてる
sns_info = User.from_omniauth(request.env["omniauth.auth"]) # User.from_omniauth は、モデルで定義(次項)
@user = sns_info[:user] # deviseのヘルパーを使うため、@user に代入(ハッシュ(モデルの返り値)から値を取得)
if @user.persisted? # ユーザー登録済み(ログイン処理)
sign_in_and_redirect @user, event: :authentication # authenticationのcallbackメソッドを呼んで、@user でログイン
set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
else # ユーザー未登録(新規登録画面へ遷移)
@sns_id = sns_info[:sns].id # ハッシュ(モデルの返り値)から取得した、認証データを一時的に保持(ユーザー登録ページに渡す)
render template: new_user_registration_url # ユーザー登録ページに遷移
end
end
参)request.env["omniauth.auth"]
に格納されてる情報
-
provider
(必須) : プロバイダー(例:twitter、facebookなど) -
uid
(必須) : プロバイダーに固有の識別子(文字列)。 -
info
(必須) : ユーザー情報(ハッシュ)- name(必須) : 表示名(通常、姓名の連結。指定子、ニックネームの場合も)。
- email : メールアドレス(一部サイトでは提供なし)
- nickname : ユーザー名(Twitterの@名、アカウント名など)
- first_name
- last_name
- location : ユーザーのロケーション(通常、市と州)。
- description : ユーザー説明文。
- image : プロフィール画像(URL、50x50ピクセルの正方形)。
- phone : 電話番号
- urls : サイト/URL識別子のキー/値のペアを含むハッシュ(例: "Blog" => "http://intridea.com/blog")
-
credentials
: アクセストークンや資格情報を提供する場合、これを通過する。- token : OAuth、OAuth 2.0プロバイダーが提供するアクセストークン。
- secret : OAuthプロバイダーが提供するアクセストークンシークレット。
- expires : アクセストークンに有効期限があるかを示すブール値
- expires_at : 有効期限(FacebookとGoogle Plusは返す。Twitter、LinkedInにはない)。
-
extra
: 追加情報(プロバイダー固有の形式)。- raw_info : 全情報のハッシュ(例:Twitterの場合、APIから返されたJSONハッシュ)。
モデル
-
User.from_omniauth
メソッドの中身を作成(ユーザー登録で渡すデータの定義)。 - sns認証したことがあれば、アソシエーションで取得し、ログイン。なければ、SNS認証時のnameとemail情報を取得。
SNS認証のコールバック時に呼ばれるメソッドを定義(user.rb)
def self.from_omniauth(auth) # snsから取得した、providerとuidを使って、既存ユーザーを検索
sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create # sns認証したことがあればアソシエーションで取得、なければSns_credentialsテーブルにレコード作成
# snsのuser or usersテーブルに対し、SNS認証で取得したメールアドレスが登録済みの場合は、取得 or なければビルド(保存はしない)
user = sns.user || User.where(email: auth.info.email).first_or_initialize(
name: auth.info.name,
email: auth.info.email
)
if user.persisted? # userが登録済みの場合:そのままログインするため、snsのuser_idを更新しとく
sns.user = user
sns.save
end
{ user: user, sns: sns } # user、snsをハッシュで返す(コントローラーがこれを受け取る)
end
- omniauth_callbackコントローラー → APIにリクエストを送る。コントローラーでの処理はないので、passthruアクション不要。
-
new_record?
: 未保存なら、trueを返す -
persisted?
: 未保存、かつ、削除履歴ナシなら、trueを返す
-
ビュー
- SNS認証用のリンクを設置。
- このリンクは、SNS認証での新規登録/ログインを兼ねてる。
例)自分で設置する場合(users/new.html.haml、devise/sessions/new.html.haml)
= link_to 'Twitterで登録', user_twitter_omniauth_authorize_path, method: :post
= link_to 'Facebookで登録', user_facebook_omniauth_authorize_path, method: :post
= link_to 'Googleで登録', user_google_oauth2_omniauth_authorize_path, method: :post
例)共通化する場合(devise/shared/_links.html.haml)
- if devise_mapping.omniauthable?
.omniauth_btn
- resource_class.omniauth_providers.each do |provider|
= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider)
- 最初のSNS認証時は、user_idがない(user_idが確定してない)。なので、sns保存には、モデルに、belongs_to user, optional: trueオプション(外部キーのnil許可)を忘れずに!
- 2回目以降のログイン時/SNS認証時は、登録済みのため、user_idカラムが更新される。
SNS認証時、PW不要にする
- SNS認証時だけ、PWを自動生成するイメージ。
- 定義した変数を使って、PW入力欄の表示/非表示を分ける。
devise/registrations/new.html.haml
- if @sns_id.present? # @sns_idがあれば、非表示(hidden_fieldで、params[:sns_auth]としてtrueを返す)
= hidden_field_tag :sns_auth, true
- else # @sns_id がなければPW入力欄を表示
.field
= f.label :password
%em (#{@minimum_password_length} 文字以上)
%br/
= f.password_field :password, autocomplete: "new-password"
.field
= f.label :password_confirmation
%br/
= f.password_field :password_confirmation, autocomplete: "new-password"
- paramsは通常、devise/registrationsのcreateアクションに送られるが、今回は、コントローラーをオーバーライドする。users/registrations_controller.rbを作成( createアクションの動作を変更(SNS認証時のみPW不要)したいため )。
- オーバーライド : 親クラスで定義したメソッドと同じ名前のメソッドを、子クラスでも定義した場合、子クラスで定義したメソッドの方が実行されること。
users/registrations_controller.rb
def create
if params[:sns_auth] == 'true'
pass = Devise.friendly_token # PWの自動生成
params[:user][:password] = pass
params[:user][:password_confirmation] = pass
end
super
end
- rubyでは、継承先クラスでsuperメソッドを使うと、親モデルの同名メソッドを実行できる(devise/registrationsのcreateアクションを呼んでくれる)。
- params[:sns_auth]が送られた時だけ、Devise.friendly_tokenでPWを自動生成し、それ以外の場合はsuperメソッドで、deviseに処理を任せる。