omniauth-login

统一网站常用注册/登陆和第三方登陆/注册.

1. 使用场景

当今第三方登陆注册平台很多,类似qq、wechat、微博这些拥有足够多的群体,自己完全重写其实稍微有点多余。那么以后所有的注册登陆注册似乎是完全可以直接使用这些即:omniauth-xxx来完成。

当然如果有必要的话还是需要写一些站内登陆。

那么站内登陆+第三方登陆的数据库设计以及相互之间的逻辑验证就显得特别混乱。那么有一种方式将站内登陆也做成一种“第三方登陆”,那么采取统一的方式来实现,就会显得特别有意义。

2. 常规登陆方式

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, Settings.github_key, Settings.github_secret
end

2.1 数据库设计

create_table "authorizations", force: :cascade do |t|
    t.integer  "user_id",    limit: 4
    t.string   "uid",        limit: 255
    t.string   "provider",   limit: 255
    t.datetime "created_at",             null: false
    t.datetime "updated_at",             null: false
end

create_table "users", force: :cascade do |t|
    t.string   "email",           limit: 255
    t.string   "name",            limit: 255
    t.string   "password_digest", limit: 255
    t.datetime "created_at",                  null: false
    t.datetime "updated_at",                  null: false
end

add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree

2.2 models

# user 
class User < ActiveRecord::Base

  has_secure_password
  has_many :authorizations
  validates :password, presence: true, allow_blank: false
  
  def authenticate(params_password)
    # super 是调用父类中的方法,而非是直接获得父类
    password_digest.present? && super(params_password)
  end
  def self.create_with_omniauth(auth_hash)
    # 如果没有返回邮箱,而又必须有邮箱作为唯一验证,随机一个!
    u = User.new(email: "#{Time.now.to_i}#{Random.rand(20)}@example.com", name: auth_hash["info"]["name"])
    u.save!(validate: false) ? u : nil
  end
end

# authorization
class Authorization < ActiveRecord::Base
  belongs_to :user
  def self.find_by_auth(auth_hash)
    find_by_provider_and_uid(auth_hash['provider'], auth_hash['uid']) ||
      create_with_omniauth(auth_hash)
  end
  def self.create_with_omniauth(auth_hash)
    u = User.create_with_omniauth(auth_hash)
    u && create! do |auth|
      auth.provider = auth_hash['provider']
      auth.uid = auth_hash['uid']
      auth.user_id = u.id
    end
  end
end

2.3 controllers


class SessionsController < ApplicationController
  def new
    @user = User.new
  end
  
  def create
  end
  
  def authenticate
    auth = Authorization.find_by_auth(auth_params)
    if auth
      user = auth.user
      render text: '登陆成功'
    else
      render text: '登陆失败'
    end
  end
  
private
  def auth_params
    auth = request.env["omniauth.auth"]
  end
  def user_params
    params.reuqire(:user).permit(:password, :email)
  end
end

备注:

普通的站内登陆的,是需要将email作为唯一性的索性,故在users表中不可出现重复邮箱!在某些第三方登陆后,不会返回给用户邮箱,或者返回邮箱是曾经已经在users表中存在。这样就无法创建user。只能自己随机一个邮箱创建user! 此时users表中的password_digest是nil.

在自己站内登陆的时候需要进行password的authentic。但是因为有涉及到因为第三方登陆导致的password_digest为nil的情况,故认证之前还需要预先判断。

3 使用omniauth-identity

3.1 配置

因为omniauth-identity没法做登陆时验证码校验,故自己修改后omniauth-identity

 Rails.application.config.middleware.use OmniAuth::Builder do
  provider :identity, on_failed_registration: lambda { |env|
      IdentitiesController.action(:new).call(env) },
      on_validation: lambda { |env|
        Captcha.valid_captcha?(env)
      }
end


class Captcha

  class << self
    def valid_captcha?(env)
      env = env[:env]
      session = env["rack.session"]
      params = env["rack.request.form_hash"]
      unless verify_rucaptcha?(params['captcha'].downcase, session['captcha'].downcase)
        env["omniauth.identity"].errors.add(:base, '验证码错误')
        session['captcha'] = nil
        return false
      end

      true
    end

    def random_text(n = 4)
      ['a'..'z', 'A'..'Z', 0..9].map(&:to_a).flatten.shuffle[0, n].join
    end

    def create(code)
      command = <<-CODE
        convert -size 100x28 -fill black -background white \
        -draw 'stroke black line #{rand(20)},#{rand(28)} #{rand(30)+50},#{rand(28)}' \
        -draw 'stroke black line #{rand(50)},#{rand(28)} #{rand(100)},#{rand(28)}' \
        -wave #{2+rand(2)}x#{50+rand(20)} \
        -font '#{Rails.root}/public/fonts/segoepr.ttf' \
        -gravity Center -sketch 3x1+#{rand(180)} -pointsize 22 -implode 0.2 label:#{code} png:-
      CODE

      sub = Subexec.run(command)
      sub.output
    end

    def verify_rucaptcha?(a, b)
      a.present? && a == b
    end

  end

end

3.2 DB

 
 ActiveRecord::Schema.define(version: 20160317122458) do
  create_table "authorizations", force: :cascade do |t|
    t.integer  "user_id",    limit: 4
    t.string   "uid",        limit: 255
    t.string   "provider",   limit: 255
    t.datetime "created_at",             null: false
    t.datetime "updated_at",             null: false
  end
  create_table "identities", force: :cascade do |t|
    t.string   "email",           limit: 255
    t.string   "password_digest", limit: 255
    t.string   "name",            limit: 255
    t.datetime "created_at",                  null: false
    t.datetime "updated_at",                  null: false
  end
  create_table "users", force: :cascade do |t|
    t.string   "email",      limit: 255
    t.string   "name",       limit: 255
    t.datetime "created_at",             null: false
    t.datetime "updated_at",             null: false
  end
end

3.3 models

## 其他同上

### 此处特别注意,不是原生的ActiveRecord::Base,是OmniAuth::Identity::Models::ActiveRecord
class Identity < OmniAuth::Identity::Models::ActiveRecord
end

3.4 views && routes


resources :identities
resources :users
resources :sessions, only: :new
match '/auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
get '/captcha', to: 'captchas#show'


# sessions/ new.html.erb

<p>
  <strong>Don't use these services?</strong>
  <%= link_to "Create an account", new_identity_path %> or login below.
</p>
<%= form_tag "/auth/identity/callback" do %>
  <div class="field">
    <%= label_tag :auth_key, "Email" %><br>
    <%= text_field_tag :auth_key %>
  </div>
  <div class="field">
    <%= label_tag :password %><br>
    <%= password_field_tag :password %>
  </div>
  <div class="actions"><%= submit_tag "Login" %></div>
<% end %>

# identities / new.html.erb

<%= form_tag "/auth/identity/register" do %>
  <% if @identity && @identity.errors.any? %>
    <div class="error_messages">
      <h2><%= pluralize(@identity.errors.count, "error") %> prohibited this account from being saved:</h2>
      <ul>
      <% @identity.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= label_tag :name %><br>
    <%= text_field_tag :name, @identity.try(:name) %>
  </div>
  <div class="field">
    <%= label_tag :email %><br>
    <%= text_field_tag :email, @identity.try(:email) %>
  </div>
  <div class="field">
    <%= label_tag :password %><br>
    <%= password_field_tag :password %>
  </div>
  <div class="field">
    <img src='/captcha'/>
    <%= text_field_tag :captcha %>
  </div>
  <div class="actions"><%= submit_tag "Register" %></div>
<% end %>