Спустя некоторое время решил посмотреть что-же еще есть на том ресурсе интересного, но в своему разочарованию сайт был не доступен. Слава кэшу Яндекса, откуда была выдернута копия того материала. Что он не пропал безвозвратно решил сделать его перевод и выложить здесь.
И так приступим…
Эта глава будет посвящена известному гему Omniauth. Omniauth это новая система идентификации поверх Rack для мультипровайдерной внешней идентификации. Он будет использован для связи CommunityGuides (прим: в настоящий момент ресурс не доступен и похоже уже не вернется) с Facebook, Google, Twitter и Github. Данная глава покажет как интегрировать все это с существующей идентификацией через Devise.
Добавляем вход через Facebook
Omniauth — система идентификации поверх Rack для мультипровайдерной внешней идентификации.
Для начала мы зарегистрируем наше приложение на Facebook developers.facebook.com/setup. Укажите имя (будет отображаться пользователям) и URL (например www.communityguides.eu/). Facebook допускает перенаправление только на зарегистрированный сайт, для разработки вам нужно указать другой URL (например http://localhost:3000/). Не указывайте в URL localhost либо 127.0.0.1 это приведет к ошибке “invalid redirect_uri”, что довольно распространено. Добавьте гем ‘omniauth’ к вашему проекту выполните bundle install, создайте инициализатор с вашим APP_ID/APP_SECRET и перезапустите сервер.
Gemfile
gem 'omniauth', '0.1.6'
config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, 'APP_ID', 'APP_SECRET' end
Теперь создадим новый контроллер и модель, которая расширит нашего пользователя различными сервисами и установит связь между ними.
Terminal
rails generate model service user_id:integer provider:string uid:string uname:string uemail:string rails generate controller services
app/models/user.rb
class User < ActiveRecord::Base devise :database_authenticatable, :oauthable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable has_many :services, :dependent => :destroy has_many :articles, :dependent => :destroy has_many :comments, :dependent => :destroy has_many :ratings, :dependent => :destroy belongs_to :country attr_accessible :email, :password, :password_confirmation, :remember_me, :fullname, :shortbio, :weburl validates :weburl, :url => {:allow_blank => true}, :length => { :maximum => 50 } validates :fullname, :length => { :maximum => 40 } validates :shortbio, :length => { :maximum => 500 } end
app/models/service.rb
class Service < ActiveRecord::Base belongs_to :user attr_accessible :provider, :uid, :uname, :uemail end
config/routes.rb
... match '/auth/facebook/callback' => 'services#create' resources :services, :only => [:index, :create] ...
Мы определили новые маршруты для сервисов (пока только index и create) и добавили так называемый маршрут для обратного вызова. Что это? Мы делаем запрос на аутентификацию пользователя через http://localhost:3000/auth/facebook. Запрос направляется на Facabook и далее Facebook перенаправляет запрос на вашу страницу используя путь /auth/facebook/callback. Мы сопоставили данный путь нашему контроллеру Services, в частности методу create. Сейчас данный метод возвращает лишь полученный хэш.
app/controllers/services_controller.rb
class ServicesController < ApplicationController def index end def create render :text => request.env["omniauth.auth"].to_yaml end end
Давайте проверим это. Перейдем по адресу http://localhost:3000/auth/facebook после чего попадем на запрос на доступ к вашим данным на Facebook. Принимаем предложение и возвращаемся в наше приложение, которое отобразит полученные данные (смотрите исходный код страницы для нормального форматирования).
Исходный код страницы
--- user_info: name: Markus Proske urls: Facebook: http://www.facebook.com/profile.php?id=.... Website: nickname: profile.php?id=.... last_name: Proske first_name: Markus uid: "..." credentials: token: ........... extra: user_hash: name: Markus Proske timezone: 1 gender: male id: "...." last_name: Proske updated_time: 2010-11-18T13:43:01+0000 verified: true locale: en_US link: http://www.facebook.com/profile.php?id=........ email: markus.proske@gmail.com first_name: Markus provider: facebook
Нас интересуют только поля id, provider name и email, расположенные в extra: user_hash. Для проверки заменим create метод следующим кодом:
app/controllers/services_controller.rb
... def create omniauth = request.env['omniauth.auth'] if omniauth omniauth['extra']['user_hash']['email'] ? email = omniauth['extra']['user_hash']['email'] : email = '' omniauth['extra']['user_hash']['name'] ? name = omniauth['extra']['user_hash']['name'] : name = '' omniauth['extra']['user_hash']['id'] ? uid = omniauth['extra']['user_hash']['id'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' render :text => uid.to_s + " - " + name + " - " + email + " - " + provider else render :text => 'Error: Omniauth is empty' end end ...
Отлично, мы сумели аутентифицировать пользователя через Facebook! Еще осталось много чего нужно сделать, мы интегрируем это в нашу схему с Devise. Есть несколько моментов, на которые нужно обратить внимание:
- Пользователь входит используя Facebook: Facebook предоставляет почту пользователя. Проверим есть ли уже такой, если нет то создаем нового пользователя к предоставленным адресом и автоматически подтверждаем. Создаем новую запись в модели Serviсe для Facebook и присваиваем созданному пользователю.
- Пользователь регистрируется или входит через Facebook первый раз, но уже имеет локального пользователя: снова получаем адрес почтуот Facebook и смотрим в нашу базу. Если мы находим такой адрес, то создаем новую записть для Facebook и связываем с найденным пользователем.
- Пользователь повторно входит через Facebook: смотрим в базу и выполняем вход для него.
Omniauth предоставляет возможность добавить больше сервисов, как мы и сделаем. Наша аутентификация завязана на почтовый адрес, поэтому только провайдеры предоставляющие его могут быть использованы. Например Github возвращает адрес только в том случаем, если пользователь указал публичный адрес. Twitter напротив никогда не показывает почтовый адрес Тем не менее, Github аккаунт с адресом может быть использован как и Fb для входа/регистрации, а Github без адреса или Twitter аккаунты могут быть добавлены к существующему локальному пользователю, либо созданного через другого провайдера.
Каждый провайдер возвращает хэш содержащий различные параметры. К сожалению, это никак не стандартизовано и каждый может давать различные имена одинакомым атрибутам. Это значит, что мы должны различать сервисы в методе create. Так же заметим, что есть только один метод для обратного вызова. Поэтому что мы должны сделать с полученными данными (войти или зарегистрировать) зависит только от нас. Изменим наш маршрут снова для всех сервисов, добавим в него параметр, в который будет помещаться имя используемого: params[:service].
config/routes.rb
... match '/auth/:service/callback' => 'services#create' resources :services, :only => [:index, :create, :destroy] ...
Далее идем на страницы для Github и Twitter. Регистрируем снова на localhost (для Twitter-а вместо localhost нужно использовать 127.0.0.1). Получим новые маршруты http://localhost:3000/auth/github/callback/ и http://127.0.0.1:3000/auth/twitter/callback. После чего изменим инициализатор.
config/initializers/omniauth.rb
# Do not forget to restart your server after changing this file Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, 'APP_ID', 'APP_SECRET' provider :twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET' provider :github, 'CLIENT ID', 'SECRET' end
Созданный метод будет проверять наличие параметра из пути и Omniauth хэша. Далее, в зависимости от сервиса аутентификации, необходимые значение из хеша переносятся в наши переменные. По крайней мере, сервис провайдер и идентификатор пользователя для него должны быть определены, иначе остановка.
Часть первая: пользователь еще не вошел: Сначала проверим, есть ли пара провайдер-идентификатор в нашей модели Service, которая подразумевает что, данная пара ассоциирована с пользователем и может быть использована для его входа. Если это так, то делаем вход. Если нет, то проверяем существование почтового адреса. Используя этот адрес, мы может найти в имеющейся модели пользователя если он уже был с ним зарегистрирован. Когда такой пользователь найдется, этот сервис будет добавлен ему и в будущем он сможет использовать его для входа. В случае если это новый почтовый адрес, то вместо этого создаем нового пользователя, подтверждаем его и добавляем данный сервис аутентификации ему.
Часть вторая: если пользователь уже вошел: Мы просто добавляем данный сервис к его аккаунту если не был добавлен ранее.
Посмотрим внимательно ниже на метод Create. Он содержит весь необходимый код для обработки различных случаев описанных выше и предоставляет идентификацию для Facebook, Github и Twitter. Заметьте, что только 4 строки кода нужны для добавления нового провайдера. Еще нету интерфейса для этого, но можете проверить перейдя по ссылкам сами:
- Facebook: http://localhost:3000/auth/facebook
- Github: http://localhost:3000/auth/github
- Twitter: http://localhost:3000/auth/twitter
- Index отображает все сервисы привязанные к текущему пользователю: http://localhost:3000/services (мы создадим эти страницы позже вместе со страницами входа и регистрации)
- метод Delete удаляет сервис.
class ServicesController < ApplicationController before_filter :authenticate_user!, :except => [:create] def index # get all authentication services assigned to the current user @services = current_user.services.all end def destroy # remove an authentication service linked to the current user @service = current_user.services.find(params[:id]) @service.destroy redirect_to services_path end def create # get the service parameter from the Rails router params[:service] ? service_route = params[:service] : service_route = 'no service (invalid callback)' # get the full hash from omniauth omniauth = request.env['omniauth.auth'] # continue only if hash and parameter exist if omniauth and params[:service] # map the returned hashes to our variables first - the hashes differ for every service if service_route == 'facebook' omniauth['extra']['user_hash']['email'] ? email = omniauth['extra']['user_hash']['email'] : email = '' omniauth['extra']['user_hash']['name'] ? name = omniauth['extra']['user_hash']['name'] : name = '' omniauth['extra']['user_hash']['id'] ? uid = omniauth['extra']['user_hash']['id'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' elsif service_route == 'github' omniauth['user_info']['email'] ? email = omniauth['user_info']['email'] : email = '' omniauth['user_info']['name'] ? name = omniauth['user_info']['name'] : name = '' omniauth['extra']['user_hash']['id'] ? uid = omniauth['extra']['user_hash']['id'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' elsif service_route == 'twitter' email = '' # Twitter API never returns the email address omniauth['user_info']['name'] ? name = omniauth['user_info']['name'] : name = '' omniauth['uid'] ? uid = omniauth['uid'] : uid = '' omniauth['provider'] ? provider = omniauth['provider'] : provider = '' else # we have an unrecognized service, just output the hash that has been returned render :text => omniauth.to_yaml #render :text => uid.to_s + " - " + name + " - " + email + " - " + provider return end # continue only if provider and uid exist if uid != '' and provider != '' # nobody can sign in twice, nobody can sign up while being signed in (this saves a lot of trouble) if !user_signed_in? # check if user has already signed in using this service provider and continue with sign in process if yes auth = Service.find_by_provider_and_uid(provider, uid) if auth flash[:notice] = 'Signed in successfully via ' + provider.capitalize + '.' sign_in_and_redirect(:user, auth.user) else # check if this user is already registered with this email address; get out if no email has been provided if email != '' # search for a user with this email address existinguser = User.find_by_email(email) if existinguser # map this new login method via a service provider to an existing account if the email address is the same existinguser.services.create(:provider => provider, :uid => uid, :uname => name, :uemail => email) flash[:notice] = 'Sign in via ' + provider.capitalize + ' has been added to your account ' + existinguser.email + '. Signed in successfully!' sign_in_and_redirect(:user, existinguser) else # let's create a new user: register this user and add this authentication method for this user name = name[0, 39] if name.length > 39 # otherwise our user validation will hit us # new user, set email, a random password and take the name from the authentication service user = User.new :email => email, :password => SecureRandom.hex(10), :fullname => name # add this authentication service to our new user user.services.build(:provider => provider, :uid => uid, :uname => name, :uemail => email) # do not send confirmation email, we directly save and confirm the new record user.skip_confirmation! user.save! user.confirm! # flash and sign in flash[:myinfo] = 'Your account on CommunityGuides has been created via ' + provider.capitalize + '. In your profile you can change your personal information and add a local password.' sign_in_and_redirect(:user, user) end else flash[:error] = service_route.capitalize + ' can not be used to sign-up on CommunityGuides as no valid email address has been provided. Please use another authentication provider or use local sign-up. If you already have an account, please sign-in and add ' + service_route.capitalize + ' from your profile.' redirect_to new_user_session_path end end else # the user is currently signed in # check if this service is already linked to his/her account, if not, add it auth = Service.find_by_provider_and_uid(provider, uid) if !auth current_user.services.create(:provider => provider, :uid => uid, :uname => name, :uemail => email) flash[:notice] = 'Sign in via ' + provider.capitalize + ' has been added to your account.' redirect_to services_path else flash[:notice] = service_route.capitalize + ' is already linked to your account.' redirect_to services_path end end else flash[:error] = service_route.capitalize + ' returned invalid data for the user id.' redirect_to new_user_session_path end else flash[:error] = 'Error while authenticating via ' + service_route.capitalize + '.' redirect_to new_user_session_path end end
Наш код полностью работоспособен и прямо сейчас можно использовать один локальный аккаунт и три сервиса для входа или регистрации. Несмотря на то что, вход и регистрация всегда проходят по одному пути /auth/service и обратный вызов всегда идет на /auth/service/callback.
Наш пример прекрасно работает, но есть недостаток, который может привести к нежелательным аккаунтам: возьмем пользователя с локальным аккаунтов (почта: one@user.com) и аккаунта в Facebook (почта: two@user.com) который уже привязан к локальному. Никаких проблем, адреса не совпадают. Если пользователь имеет Google аккаунт с почтой: three@user.com, то он может быть привязан без проблем пока сессия активна. С другой стороны, предположим, что пользователь никогда не связывал Google аккаунт и он еще не вошел: если он нажмет на “войти через Google” наш create метод выполнит поиск для three@user.com, ничего не найдет и создаст нового пользователя.
Пришло время добавить пару вьюшек, начнем с входа и регистрации:
<section id="deviseauth"> <h2>Sign in</h2> <h3>Sign in with your CommunityGuides account -- OR -- use an authentication service</h3> <div id="local" class="box"> <%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %> <p><%= f.label :email %><br /> <%= f.text_field :email %></p> <p><%= f.label :password %><br /> <%= f.password_field :password %></p> <% if devise_mapping.rememberable? %> <p><%= f.check_box :remember_me %> <%= f.label :remember_me %></p> <% end %> <p><%= f.submit "Sign in" %></p> <% end %> </div> <div id="remote"> <div id="terms" class="box"> <%= link_to "Terms of Service", "#" %> </div> <div id="services" class="box"> <a href="/auth/facebook" class="services"><%= image_tag "facebook_64.png", :size => "64x64", :alt => "Facebook" %>Facebook</a> <a href="/auth/google" class="services"><%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>Google</a> <a href="/auth/github" class="services"><%= image_tag "github_64.png", :size => "64x64", :alt => "Github" %>Github</a> <a href="/auth/twitter" class="services"><%= image_tag "twitter_64.png", :size => "64x64", :alt => "Twitter" %>Twitter</a> </div> </div> <div id="devise_links"> <%= render :partial => "devise/shared/links" %> </div> </section>
<section id="deviseauth"> <h2>Sign up</h2> <h3>Sign up on CommunityGuides manually -- OR -- or use one of your existing accounts</h3> <div id="local2" class="box"> <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> <%= devise_error_messages! %> <p><%= f.label :email %><br /> <%= f.text_field :email %></p> <p><%= f.label :password %><br /> <%= f.password_field :password %></p> <p><%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %></p> <p><%= recaptcha_tags %></p> <p><%= f.submit "Sign up" %></p> <% end %> </div> <div id="remote2"> <div id="terms" class="box"> <%= link_to "Terms of Service", "#" %> </div> <div id="services" class="box"> <a href="/auth/facebook" class="services2"><%= image_tag "facebook_64.png", :size => "64x64", :alt => "Facebook" %>Facebook</a> <a href="/auth/google" class="services2"><%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>Google</a> <a href="/auth/github" class="services2"><%= image_tag "github_64.png", :size => "64x64", :alt => "Github" %>Github*</a> <div id="footnote_signup">* You can use Github only if you set a public email address</div> </div> </div> <div id="devise_links"> <%= render :partial => "devise/shared/links" %> </div> </section>
Вы можете скачать изображения Github:Authbuttons. Сейчас наши пользователи могут входить или регистрироваться через удобный интерфейс. В дополнение, нам нужна страница с настройками, где пользователи смогут управлять аккаунтами связанными с локальным.
<section id="deviseauth"> <h2>Authentication Services - Setting</h2> <div id="currservices"> <h3>The following <%= @services.count == 1 ? 'account is' : 'accounts are' %> connected with your local account at CommunityGuides:</h3> <% @services.each do |service| %> <div class="services_used"> <%= image_tag "#{service.provider}_64.png", :size => "64x64" %> <div class = "user"> <div class="line1">Name: <%= service.uname %> (ID: <%= service.uid %>)</div> <div class="line2">Email: <%= service.uemail != '' ? service.uemail : 'not set' %></div> <div class="line3"> <% @services.count == 1 ? @msg = 'Removing the last account linked might lock you out of your account if you do not know the email/password sign-in of your local account!' : @msg = '' %> <%= link_to "Remove this service", service, :confirm => 'Are you sure you want to remove this authentication service? ' + @msg, :method => :delete, :class => "remove" %> </div> </div> </div> <% end %> </div> <div id="availableservices"> <h3>You can connect more services to your account:</h3> <div id="services"> <a href="/auth/facebook" class="services"><%= image_tag "facebook_64.png", :size => "64x64", :alt => "Facebook" %>Facebook</a> <a href="/auth/google" class="services"><%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>Google</a> <a href="/auth/github" class="services"><%= image_tag "github_64.png", :size => "64x64", :alt => "Github" %>Github</a> <a href="/auth/twitter" class="services"><%= image_tag "twitter_64.png", :size => "64x64", :alt => "Twitter" %>Twitter</a> </div> <h4>If you signed-up for CommunityGuides via an authentication service a random password has been set for the local password. You can request a new password using the "Forgot your Password?" link on the sign-in page.</h4> </div> </section>
Добавляем Google
Наконец давайте добавим Google к списку наших сервис провайдеров. Google (и OpenID в частности) требуют постоянного хранилища. Вы можете использовать ActiveRecord или файловую систему как показано ниже. Если вы хотите разворачивать на Heroku, помните, что у вас нету доступа на запись в /tmp. Хотя, как отмечено в Heroku Docs, вы можете писать в ./tmp.
Две строчки конфигураций и четыре для присвоения значений из хеша — это все что нужно для добавления авторизации через Google в вашем коде. Это ли не великолепно? Достаточно Omniauth на сегодня, но если вы хотите использовать его в одном из ваших проектом, вы можете найти много ресурсов в Omniauth Wiki, также Райна Бэйтс сделал великолепные скринкасты по нему.
Вновь настроим Devise
Существует небольшой недостаток в профиле наших пользователей. Пльзователю нужно вводить текущий пароль для смены настроек. Если он зарегистрирован через один из сервисов, то он не имеет пароля, помните, мы устанавливали его в случайную строку. В Devise Wiki есть статья с тем как полностью убрать пароль. Но у себя мы хотим оставить пароль только для локальных пользователей. Для остальных пользователей разрешим менять свой профить без использования пароля. В дополнение, они смогут установить локальный пароль если захотят. Это достигается путем модификации метода update для контроллера регистрации:
... def update # no mass assignment for country_id, we do it manually # check for existence of the country in case a malicious user manipulates the params (fails silently) if params[resource_name][:country_id] resource.country_id = params[resource_name][:country_id] if Country.find_by_id(params[resource_name][:country_id]) end if current_user.haslocalpw super else # this account has been created with a random pw / the user is signed in via an omniauth service # if the user does not want to set a password we remove the params to prevent a validation error if params[resource_name][:password].blank? params[resource_name].delete(:password) params[resource_name].delete(:password_confirmation) if params[resource_name][:password_confirmation].blank? else # if the user wants to set a password we set haslocalpw for the future params[resource_name][:haslocalpw] = true end # this is copied over from the original devise controller, instead of update_with_password we use update_attributes if resource.update_attributes(params[resource_name]) set_flash_message :notice, :updated sign_in resource_name, resource redirect_to after_update_path_for(resource) else clean_up_passwords(resource) render_with_scope :edit end end end ...
Код использует дополнительное поле в пользовательской модели, вы можете вернуть и добавить его в миграцию (t.boolean :haslocalpw, :null => false, :default => true), измените модель для разрешения массового присваивания для этого поля, измените вьюшку чтобы скрыть поле для ввода текущего пароля если haslocalpw ложно и изменим create метод нашего service контроллера для установки этого поля при создании пользователя:
app/controllers/services_controller.rb
... user = User.new :email => email, :password => SecureRandom.hex(10), :fullname => name, :haslocalpw => false ...
PS: это первый мой большой перевод, поэтому просьба ошибки/кривые формулировки в личку. Большое спасибо.
ссылка на оригинал статьи http://habrahabr.ru/post/166325/
Добавить комментарий