2016年4月17日 星期日

Published 中午12:28 by with 3 comments

Rails User 建立-1 (Devise + Omniauth-facebook + facebook api) - 我在ALPHACamp 56天

今天在做專案中, 我負責使用者的部分

我會用到 Devise + Omniauth-facebook  + facebook api

好不容易才搞懂... 趕快做個筆記

Devise 的部分可以參考 iHower 實戰聖經 - 使用者認證 

為了版面的完整度... 我還是貼code一下好了


- Devise -


1. 在 Gemfile 新增:


 gem 'devise'
2. bundle install

3. rails g devise:install產生devise設定檔

4. config/environments/development.rb 和 production.rb 加入寄信時預設的網站網址: 

config.action_mailer.default_url_options = { :host => 'localhost:3000' }
5. 確認 app/views/layouts/application.html.erb layout 中可以顯示 flash 訊息

 <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
6.確認 routes.rb 中有設定網站首頁位置

 root :to => "welcome#index"
7.輸入rails g devise user產生 User model 及 Migration

8.輸入rails generate devise:views產生樣板

9. 輸入bin/rake db:migrate建立資料表

10. 在需要登入的 controller 加上before_action :authenticate_user!

before_action :authenticate_user!, :except => [:index]
註:index 頁面不需要權限就可以瀏覽, 要在後方補上 :except => [:index] 

11. 在Layout 中加上 登入 / 登出選單

    <% if current_user %>
          <%= link_to('登出', destroy_user_session_path, :method => :delete) %> |
          <%= link_to('修改密碼', edit_registration_path(:user)) %>
      <% else %>
          <%= link_to('註冊', new_registration_path(:user)) %> |
          <%= link_to('登入', new_session_path(:user)) %>
      <% end %>

以上內容出自於 iHower 實戰聖經 - 使用者認證  若有不懂請入內參考!


- Omniauth -


Omniauth 是一套使用者認證系統, 針對不同網站的使用者有不統的gem可以使用, 可以透過該平台使用者帳號登入, 使用者與站方, 也不用擔心儲存密碼的安全性問題

首先要先到 developers.facebook 取得 facebook 的 APP_ID + APP_SECRET

1. 點選右上角, 新增應用程式



2. 選擇網頁



3. 新增名稱




 4. 輸入資料

5. 我把以下的code 貼到  app/views/layouts/application.html.erb 


7.  開發用的 Site URL 請填 http://localhost:3000

8. 取得  APP_ID + APP_SECRET 




接下來要使用的是  omniauth-facebook gem <=內有詳細步驟

我這邊整理一下我的做法:

1. 在 Gemfile 新增:


 gem 'omniauth-facebook'
2. bundle

3. 新增 migration : rails g migration AddOmniauthToUsers

 class AddOmniauthToUsers < ActiveRecord::Migration 
     def change
          add_column :users, :fb_uid, :string 
          add_column :users, :fb_token, :string 

          add_index :users, :fb_uid 
      end 
end
4. rake db:migrate

5. 在config/initializers/devise.rb 底下

config.omniauth :facebook, "輸入剛剛得到的APP_ID", "輸入剛剛得到的APP_SECRET", :scope => 'public_profile,email', :info_fields => 'email,name'
6. 編輯 app/models/user.rb,加上:omniauthable, :omniauth_providers => [:facebook]如下:


devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable,
       :omniauthable, :omniauth_providers => [:facebook]
7. 編輯 config/routes.rb

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
8. 編輯 app/views/layout/application.html.erb,加上 Facebook 登入的超連結:

<% if current_user %>
  <%= link_to('登出', destroy_user_session_path, :method => :delete) %>
  ....
<% else %>
    ....
    <%= link_to "登入 Facebook", user_omniauth_authorize_path(:facebook) %>
<% end %>
9. 新增 app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def facebook
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end

end

10.  在 app/models/user.rb,新增一個類別方法self.from_omniauth(auth)

  def self.from_omniauth(auth)
     # Case 1: Find existing user by facebook uid
     user = User.find_by_fb_uid( auth.uid )
     if user
        user.fb_token = auth.credentials.token
        #user.fb_raw_data = auth
        user.save!
       return user
     end

     # Case 2: Find existing user by email
     existing_user = User.find_by_email( auth.info.email )
     if existing_user
       existing_user.fb_uid = auth.uid
       existing_user.fb_token = auth.credentials.token
       #existing_user.fb_raw_data = auth
       existing_user.save!
       return existing_user
     end

     # Case 3: Create new password
     user = User.new
     user.fb_uid = auth.uid
     user.fb_token = auth.credentials.token
     user.email = auth.info.email
     user.password = Devise.friendly_token[0,20]
     #user.fb_raw_data = auth
     user.save!
     return user
   end


- Facebook API -

OK~ 如果貼得順利 , 目前你已經可以用FB 登入你的web

接下來才會是今天的重點 facebook api !

貼code先提到觀念

API 也是採用 MVC概念

Model:
           部分主要是儲存我們要傳送的資料 

View:
        在API中的view 不會真的被看到, 我們會透過 xxx.json.jbuilder ,填寫要傳送的資料, 
可以想像一下, 當手機按下那個按鍵時, 其實是發了一個request 給後端, 後端傳json檔案格式給前端, 裡面資料就包含該 action內的資料.



Controller:
                會把對應的controller 寫在 api/v1底下.


再來講使用者這部分的觀念


a.一開始,使用者註冊, 會得到一組access_token, 之後這組 access_token 就存放在APP內
    (可以說用 email+password 去換一組 token, 後台認可的token)

b.之後APP連接後台時, 就會拿這組 access_token 來確認使用者

c. 登出, 並不會刪除access_token 而是在後台把這組email+password 一組新的access_token, 此時手機上的access_token 不等於後台的access_token, 所以就登出了

實作:

1. 新增 app/models/user.rb 

 before_create :generate_authentication_token

   def generate_authentication_token
     self.authentication_token = Devise.friendly_token
   end
2. rails g migration add_token_to_users.rb

 class AddTokenToUsers < ActiveRecord::Migration

   def change
     add_column :users, :authentication_token, :string
     add_index :users, :authentication_token, :unique => true
 
     User.find_each do |u|
       puts "generate user #{u.id} token"
       u.generate_authentication_token
       u.save!

     end
   end
 end
3. 執行 rake db:migrate

4. 修改 app/controllers/api_controller.rb 

 class ApiController < ActionController::Base
  
   before_action :authenticate_user_from_token!
  
     def authenticate_user_from_token!
 
     if params[:auth_token].present?
       user = User.find_by_authentication_token( params[:auth_token] )
 
       sign_in(user, store: false) if user

     end
   end
  
  end
5. 新增 app/controllers/api_v1/auth_controller.rb



class ApiV1::AuthController < ApiController
 
   before_action :authenticate_user!, :only => [:logout]
 
   def login
     user = User.find_by_email( params[:email] )
 
     if user && user.valid_password?( params[:password] )
       render :json => { :user_id => user.id,
                         :auth_token => user.authentication_token }
     else
       render :json => { :message => "email or password is not correct" }, :status => 401
     end
 
   end
 
   def logout
     user = current_user
 
     user.generate_authentication_token
     user.save!
 
     render :json => { :message => "ok" }
   end
 
 end
6. 在 config/routes.rb 的 scope :path => '/api/v1/', :module => "api_v1", :as => 'v1', :defaults => { :format => :json } do 下方新增 



post "/login" => "auth#login"
post "/logout" => "auth#logout"
7. 修改在 app/controllers/api_v1/auth_controller.rb 中的 def login 把 user = User.find_by_email( params[:email] ) 替換成 



    success = false
 
     if params[:email] && params[:password]
       user = User.find_by_email( params[:email] )
       success = user && user.valid_password?( params[:password] )
     elsif params[:access_token]
       fb_data = User.get_fb_data( params[:access_token] )
       if fb_data
         auth_hash = OmniAuth::AuthHash.new({
           uid: fb_data["id"],
           info: {
             email: fb_data["email"]
           },
           credentials: {
             token: params[:access_token]
           }
         })
         user = User.from_omniauth(auth_hash)
       end
       success = fb_data && user.persisted?
     end 
      
     user = User.find_by_email( params[:email] )
下方 


    if user && user.valid_password?( params[:password] )
      render :json => { :user_id => user.id,
                        :auth_token => user.authentication_token }
替換成:


    if success
      render :json => { :auth_token => user.authentication_token,
                        :user_id => user.id}
8. 最後在  app/models/user.rb 新增 (若原本有請修改成以下)

  def self.get_fb_data(access_token)
    res = RestClient.get "https://graph.facebook.com/v2.4/me",  { :params => { :access_token => access_token } }
 
     if res.code == 200
       JSON.parse( res.to_str )
     else
       Rails.logger.warn(res.body)
       nil
     end  
 end
詳細code 參考  ihower github (以下)




最後在View資料夾中 寫我們要回傳得數值 (使用 jbuilder 格式)


-Postman-


關於測試我們可以使用 postman  <=Chorme 應用程式

1. 測試登入 =>有得到 auth_token



2. 測試 logout =>得到回覆 ok
      edit

3 則留言: