首先这是个蛋疼的需求,一般来说,远程做 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/