Существует довольно много OAuth решений, но я хочу поделиться тем, что использую лично, так как оно позволяет грамотно связать несколько провайдеров(twitter, fb и тд) с одним пользователем. Если вы пользуетесь готовыми решениями с интернета, вы обнаружите, что каждый раз, когда пользователь логинится с другого OAuth провайдера, создается новый аккаунт, что приводит в замешательство. Так же некоторые провайдеры не делятся email адресом пользователя, поэтому нам придется добавить дополнительный шаг в регистрацию, подробнее об этом здесь.
Начальная реализация
Gemfile
gem 'devise' gem 'omniauth' gem 'omniauth-twitter' gem 'omniauth-facebook' gem 'omniauth-vkontakte'
Генерируем миграции и модели
rails generate devise:install rails generate devise user rails g migration add_name_to_users name:string rails g model identity user:references provider:string uid:string # Отредактируйте db/migrate/[timestamp]_add_devise_to_users.rb для установки Devise модулей, которые вы будете использовать. # Я обычно использую "confirmable" модуль, для регистраций через email.
app/models/identity.rb
class Identity < ActiveRecord::Base belongs_to :user validates_presence_of :uid, :provider validates_uniqueness_of :uid, :scope => :provider def self.find_for_oauth(auth) find_or_create_by(uid: auth.uid, provider: auth.provider) end end
app/config/initializers/devise.rb
Devise.setup do |config| ... config.omniauth :facebook, "KEY", "SECRET" config.omniauth :twitter, "KEY", "SECRET" config.omniauth :vkontakte, "KEY", "SECRET" ... end
config/environments/[environment].rb
... # General Settings config.app_domain = 'somedomain.com' # Email config.action_mailer.delivery_method = :smtp config.action_mailer.perform_deliveries = true config.action_mailer.default_url_options = { host: config.app_domain } config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', port: '587', enable_starttls_auto: true, user_name: 'someuser', password: 'somepass', authentication: :plain, domain: 'somedomain.com' } ...
config/routes.rb
... devise_for :users, :controllers => { omniauth_callbacks: 'omniauth_callbacks' } ...
app/controllers/omniauth_callbacks_controller.rb
Стоит отметить, что единственный безопасный критерий соответствия пользователя с OAuth провайдером - это подтвержденный email адрес, но это приведет к созданию нескольких аккаунтов, если пользователь имеет разные email адреса для разных OAuth провайдеров. К примеру, пользователь зарегистрировался через facebook, затем позже попытался залогиниться через вконтакте, с которым связан другой email адрес, в таком случае система создаст новый аккаунт, потому что не найдет совпадений с уже существующим пользователем.
Поэтому, что бы связать аккаунт с несколькими OAuth провайдерами, current_user
сессия должна уже быть установлена при возврате OAuth коллбэка, и передана через User.find_for_oauth
. Это может прозвучать сложно, однако что бы прикрепить еще один провайдер, скажем фейсбук, это redirect_to user_omniauth_authorize_path(:facebook)
, когда пользователь уже залогинен.
class OmniauthCallbacksController < Devise::OmniauthCallbacksController def self.provides_callback_for(provider) class_eval %Q{ def #{provider} @user = User.find_for_oauth(env["omniauth.auth"], current_user) if @user.persisted? sign_in_and_redirect @user, event: :authentication set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format? else session["devise.#{provider}_data"] = env["omniauth.auth"] redirect_to new_user_registration_url end end } end [:twitter, :facebook, :vkontakte].each do |provider| provides_callback_for provider end def after_sign_in_path_for(resource) if resource.email_verified? super resource else finish_signup_path(resource) end end end
app/models/user.rb
class User < ActiveRecord::Base TEMP_EMAIL_PREFIX = 'change@me' TEMP_EMAIL_REGEX = /\Achange@me/ # Include default devise modules. Others available are: # :lockable, :timeoutable devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable validates_format_of :email, :without => TEMP_EMAIL_REGEX, on: :update def self.find_for_oauth(auth, signed_in_resource = nil) # Получить identity пользователя, если он уже существует identity = Identity.find_for_oauth(auth) # Если signed_in_resource предоставлен оно всегда переписывает существующего пользователя # что бы identity не была закрыта случайно созданным аккаунтом. # Заметьте, что это может оставить зомби-аккаунты (без прикрепленной identity) # которые позже могут быть удалены user = signed_in_resource ? signed_in_resource : identity.user # Создать пользователя, если нужно if user.nil? # Получить email пользователя, если его предоставляет провайдер # Если email не был предоставлен мы даем пользователю временный и просим # пользователя установить и подтвердить новый в следующим шаге через UsersController.finish_signup email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email) email = auth.info.email if email_is_verified user = User.where(:email => email).first if email # Создать пользователя, если это новая запись if user.nil? user = User.new( name: auth.extra.raw_info.name, #username: auth.info.nickname || auth.uid, email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", password: Devise.friendly_token[0,20] ) user.skip_confirmation! user.save! end end # Связать identity с пользователем, если необходимо if identity.user != user identity.user = user identity.save! end user end def email_verified? self.email && self.email !~ TEMP_EMAIL_REGEX end end
Завершение регистрации
Большинство OAuth провайдеров предоставляют нам всю необходимую информацию, но если пользователь зарегистрировался через твитер, или по какой-то иной причине OAuth провайдер не предоставил нам email, или вы просто хотите получить какую-то дополнительную информацию от пользователя, тогда нам надо добавить дополнительный шаг.
config/routes.rb
... match '/users/:id/finish_signup' => 'users#finish_signup', via: [:get, :patch], :as => :finish_signup ...
app/controllers/users_controller.rb
Если вы используете Devise confirmable модуль, для подтверждения email, тогда вы возможно захотите пропустить шаг валидации email, что бы не лишать пользователя всей радости регистрации через OAuth. Если вы все же хотите заставить пользователя подтвердить своей email адрес, тогда просто закомментируйте строчку current_user.skip_reconfirmation!
class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] ... # GET /users/:id.:format def show # authorize! :read, @user end # GET /users/:id/edit def edit # authorize! :update, @user end # PATCH/PUT /users/:id.:format def update # authorize! :update, @user respond_to do |format| if @user.update(user_params) sign_in(@user == current_user ? @user : current_user, :bypass => true) format.html { redirect_to @user, notice: 'Your profile was successfully updated.' } format.json { head :no_content } else format.html { render action: 'edit' } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # GET/PATCH /users/:id/finish_signup def finish_signup # authorize! :update, @user if request.patch? && params[:user] #&& params[:user][:email] if @user.update(user_params) @user.skip_reconfirmation! sign_in(@user, :bypass => true) redirect_to @user, notice: 'Your profile was successfully updated.' else @show_errors = true end end end # DELETE /users/:id.:format def destroy # authorize! :delete, @user @user.destroy respond_to do |format| format.html { redirect_to root_url } format.json { head :no_content } end end private def set_user @user = User.find(params[:id]) end def user_params accessible = [ :name, :email ] # extend with your own params accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank? params.require(:user).permit(accessible) end end
app/views/users/finish_signup.html.erb
В этой версии форма просит только email адрес пользователя, вы так же можете добавить и другие необходимые поля, и даже запросить пользователя указать пароль, в таком случае в будущем он сможет логиниться через email+пароль. Заметье, что шаблон ниже использует разметку бутстрапа.
<div id="add-email" class="container"> <h1>Add Email</h1> <%= form_for(current_user, :as => 'user', :url => finish_signup_path(current_user), :html => { role: 'form'}) do |f| %> <% if @show_errors && current_user.errors.any? %> <div id="error_explanation"> <% current_user.errors.full_messages.each do |msg| %> <%= msg %><br> <% end %> </div> <% end %> <div class="form-group"> <%= f.label :email %> <div class="controls"> <%= f.text_field :email, :autofocus => true, :value => '', class: 'form-control input-lg', placeholder: 'Example: email@me.com' %> <p class="help-block">Please confirm your email address. No spam.</p> </div> </div> <div class="actions"> <%= f.submit 'Continue', :class => 'btn btn-primary' %> </div> <% end %> </div>
app/controllers/application_controller.rb
Следующий метод опциональный, но он полезен, если вы хотите убедиться, что пользователь предоставил всю необходимую информацию.
Вы можете использовать его как before_filter
, следующим образом: before_filter :ensure_signup_complete, only: [:new, :create, :update, :destroy]
class ApplicationController < ActionController::Base ... def ensure_signup_complete # Убеждаемся, что цикл не бесконечный return if action_name == 'finish_signup' # Редирект на адрес 'finish_signup' если пользователь # не подтвердил свою почту if current_user && !current_user.email_verified? redirect_to finish_signup_path(current_user) end end end
На этом все. Если я что-нибудь упустил, пожалуйста сообщите мне и я быстро поправлю.
Данная статья является переводом. Оригинал