Rails Devise 使用远程 API 登录

sanvi · 2013年09月16日 · 最后由 zj0713001 回复于 2013年09月16日 · 4303 次阅读

首先这是个蛋疼的需求,一般来说,远程做 OAuth2 就行了,但是有很多因素导致(历史遗留问题,老系统等),所以就出现了需要在新的系统上去使用老系统的登录 API

#app/models/user.rb

class User
  include Mongoid::Document
  include Mongoid::Timestamps
  include ActiveModel::Validations #required because some before_validations are defined in devise
  extend ActiveModel::Callbacks #required to define callbacks
  extend Devise::Models
  devise :remote_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :authentication_keys => [:user_id]
  # Setup accessible (or protected) attributes for your model
  attr_accessible :id, :remember_me, :user_id, :dept_id, :user_id
  field :remember_token
end

首先,你的 User 变成类似如上的格式,把默认的 database_authenticatable 替换成 remote_authenticatable,remote_authenticatable 稍后我们会给出代码

#lib/remote_authenticatable.rb

require 'bcrypt'

module Devise
  module Models
    module RemoteAuthenticatable
      extend ActiveSupport::Concern

      include BCrypt


      def email_required?
          false
      end

      def password_required?
       true
      end

      module ClassMethods
        def serialize_from_session(key, salt)
          resource = User.where(:id => key).first
          resource
        end
        def serialize_into_session(record)
          [record.id, record.user_id]
        end
      end

      def password_digest(password)
        ::BCrypt::Password.create("#{password}#{Devise.pepper}", :cost => Devise.stretches).to_s
      end


      def remote_authentication(authentication_hash)

        # Your logic to authenticate with the external webservice
        return false if !authentication_hash or !authentication_hash[:user]
        # These four parameters are required by the authentication mechanism
        # id/password actually authenticate the user
        # org_id/term_id identify the remote server to perform auth against

        @user_id = authentication_hash[:user][:user_id]
        @dept_id = authentication_hash[:user][:dept_id]
        @password = authentication_hash[:user][:password]
        if authentication_hash[:user][:remember_me] == "1"
          self.remember_me = true
        else
          self.remember_me = false
        end
        self.remember_token ||= Devise.friendly_token


        # return false if @id.length > 3

        # Perform remote authentication here

        # The remote auth mechanism returns the user's display name
        # and access control list
        # @name = "Display Name"
        # @acl = "Access Control List"

        # ret = @name != nil and @acl != nil

        require 'open-uri'
        require 'nokogiri'

        doc = Nokogiri::HTML(open("http://xxx.com/login"))
        ret = doc.xpath("//status").first.text

        if ret == "false"
          self.errors.add(:password, doc.xpath("//message").first.text)
          return false
        end

        self.user_id = @user_id
        self.emp_id = doc.xpath("//emp_id").first.text
        self.encrypted_password = self.password_digest(@password)


        return true if ret == "true"

      end
    end
  end

  module Strategies
    class RemoteAuthenticatable < Authenticatable
      def valid?
        true
      end

      #
      # For an example check : https://github.com/plataformatec/devise/blob/master/lib/devise/strategies/database_authenticatable.rb
      #
      # Method called by warden to authenticate a resource.
      #
      def authenticate!

        #
        # mapping.to is a wrapper over the resource model
        #
        if params and params[:user]
          resource =  mapping.to.where(:user_id => params[:user][:user_id]).first
        end
        if resource.blank?
          resource = mapping.to.new
        end

        return fail! unless resource

        # validate is a method defined in Devise::Strategies::Authenticatable. It takes
        #a block which must return a boolean value.
        #
        # If the block returns true the resource will be logged in
        # If the block returns false the authentication will fail!
        #
        if validate(resource){ resource.remote_authentication(params) }
          success!(resource)
        end
      end
    end
  end
end

当你登录的时候,他会先调用 RemoteAuthenticatable 的 valid?,接着会调用 authenticate!

# 会在本地创建一个User
  # mapping.to is a wrapper over the resource model
  #
  if params and params[:user]
    resource =  mapping.to.where(:user_id => params[:user][:user_id]).first
  end
  if resource.blank?
    resource = mapping.to.new
  end

  return fail! unless resource

然后会调用 remote_authentication,RemoteAuthenticatable 的 self 可以理解为 User 本身,在里面我们写了一个 password_digest(PS.这个方法从 devise 里面弄出来得),目的是要把密码重新加密放到本地的数据库里面,接着我们就 call 远程的 API,这里就不用多说了。

#config/initializers/devise.rb

require 'remote_authenticatable'

config.warden do |manager|
  manager.strategies.add(:remote, Devise::Strategies::RemoteAuthenticatable)
  manager.default_strategies(:scope => :user).unshift :remote
end

Warden::Manager.serialize_into_session { |user| YAML::dump(user) }
Warden::Manager.serialize_from_session { |yaml| YAML::load(yaml) }

Devise.add_module :remote_authenticatable, :controller => :sessions, :route => { :session => :routes }

在 devise 的配置加入一个 module,使其工作

最后,讲得大部分是实现,如果有兴趣深入的同学可以移步 http://4trabes.com/2012/10/31/remote-authentication-with-devise/

图画的很萌

同楼上 图画的真不错 有意思 可是我作为一个重度强迫症患者 觉得有些地方很不舒服 第一 好几个地方的对齐不对 第二 User.where(:id => key).first 这个我习惯写成 User.find_by_id key

#1 楼 @nightire 随手转过来的= =

#2 楼 @zj0713001 我以前用 find 的时候经常会报错,所以现在就比较喜欢直接用 where

#4 楼 @sanivbyfish 哈哈 每个人强迫症的发病情况都不一样

需要 登录 后方可回复, 如果你还没有账号请 注册新账号