让Devise AJAX登录使用确认

时间:2013-10-31 13:15:34

标签: ruby-on-rails devise devise-confirmable

我们正在尝试扩展Devise(3.1.1)登录/注册方法来处理AJAX请求,但是我们仍然坚持使用可确认的逻辑。通常情况下,如果用户在确认其帐户之前登录设计,他们将被重定向到登录屏幕并显示以下消息:“您必须先确认帐户才能继续。”我们不能弄清楚Devise正在检查确认并做出重定向的决定。

这是我们的扩展 sessions_controller 代码。它适用于成功和失败的登录尝试:

  # CUSTOM (Mix of actual devise controller method and ajax customization from http://natashatherobot.com/devise-rails-sign-in/):
  def create
    # (NOTE: If user is not confirmed, execution will never get this far...)
    respond_to do |format|
      format.html do 
        # Copied from original create method:
        self.resource = warden.authenticate!(auth_options)
        set_flash_message(:notice, :signed_in) if is_navigational_format?
        sign_in(resource_name, resource)
        respond_with resource, :location => after_sign_in_path_for(resource)
      end                       
      format.js  do
        # Derived from Natasha AJAX recipe:
        self.resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
        sign_in(resource_name, resource)
        return render :json => {:success => true, :token => form_authenticity_token() }, content_type: "application/json" # Need to explicitely set content type to JSON, otherwise it gets set as application/javascript and success handler never gets hit.
      end          
    end

  end

  def failure
    return render :json => {:success => false, :errors => ["Login failed."]}
  end

问题是,如果用户未经证实,create方法永远不会被击中。重定向发生在之前的某个地方,这意味着我们无法以JS友好的方式处理它。但是通过源代码我找不到任何可以进行确认检查的过滤器。可确认的检查在哪里发生,我们如何拦截呢?

4 个答案:

答案 0 :(得分:8)

正在发生的事情是sign_in方法通过抛出一个warden错误来打破你的正常流程,这会调用失败的应用程序。

如果您查看sign_inlib/devise/controllers/helpers.rb的定义,您会看到在第一次登录用户的正常流程中,您最终会调用

warden.set_user(resource, options.merge!(:scope => scope)

warden是对Warden::Proxy对象的引用,如果您查看set_user的作用(您可以在warden/lib/warden/proxy.rb:157-182看到),您会看到将用户序列化到会话后,它会运行任何after_set_user回调。

Devise在lib/devise/hooks/中定义了大量这些内容,我们感兴趣的特定内容位于lib/devise/hooks/activatable.rb中:

Warden::Manager.after_set_user do |record, warden, options|
  if record && record.respond_to?(:active_for_authentication?) && !record.active_for_authentication?
    scope = options[:scope]
    warden.logout(scope)
    throw :warden, :scope => scope, :message => record.inactive_message
  end
end

如您所见,如果记录不是active_for_authentication?,那么我们throw。这就是您的情况 - active_for_authentication?对于尚未确认的confirmable资源返回false(请参阅lib/devise/models/confirmable.rb:121-127)。

当我们throw :warden时,我们最终会调用设计failure_app。这就是正在发生的事情,以及为什么你要打破控制器的正常控制流程。

(实际上上面是谈论正常的会话控制器流程。我认为你的js块实际上是多余的 - 调用warden.authenticate!也会设置用户,所以我认为你扔了在你到达sign_in之前。)

要回答第二个问题,处理此问题的一种可能方法是创建自己的失败应用。默认设置将warden的failure_app设置为Devise::Delegator,这允许您为不同的设计模型指定不同的故障应用程序,但如果未配置任何设备,则默认为Devise::FailureApp。您可以自定义现有的故障应用程序,通过配置warden将其替换为您自己的故障应用程序,也可以自定义委托者以使用html请求的默认故障应用程序并委托给json的其他故障应用程序。

答案 1 :(得分:3)

@gregates因为搞清楚这一点而获得赞誉,但我想展示我是如何实现这一目标的:

问题是您无法在控制器级别检查或拦截未经证实的用户,因为它会在模型​​级别使用active_for_authentication?进行检查,然后立即由Warden的失败应用程序处理。因此,对current_user的任何引用都会使您正在进行的控制器逻辑短路。解决方案是引入您自己的自定义FailureApp,并根据需要处理事情。在我们的例子中,这意味着为AJAX auth错误引发一个特殊错误。这是我们如何做到的:

首先,创建一个自定义失败应用程序,如下所述:
How To: Redirect to a specific page when the user can not be authenticated · plataformatec/devise Wiki

lib / custom_failure.rb:

class CustomFailure < Devise::FailureApp

  def respond

    # We override Devise's handling to throw our own custom errors on AJAX auth failures,
    # because Devise provides no easy way to deal with them:

    if request.xhr? && warden_message == :unconfirmed
      raise MyCustomErrors::AjaxAuthError.new("Confirmation required. Check your inbox.")
    end

    # Original code:

    if http_auth?
      http_auth
    elsif warden_options[:recall]
      recall
    else
      redirect
    end
  end

end

然后告诉Warden使用自定义失败应用程序:

<强>配置/初始化/ devise.rb:

  config.warden do |manager|
    manager.failure_app = CustomFailure
  end

然后你只需按照自己喜欢的方式处理自定义错误。在我们的例子中,我们让ErrorController返回401 JSON响应。

答案 2 :(得分:2)

我之前遇到过同样的问题。实际上它可以很简单地解决。

转到config/initializers/devise.rb,找到以下行

# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming his account. For instance, if set to 2.days, the user will be
# able to access the website for two days without confirming his account,
# access will be blocked just in the third day. Default is 0.days, meaning
# the user cannot access the website without confirming his account.
config.allow_unconfirmed_access_for = 0.days

激活最后一行并根据需要设置句点,例如7.days

此期间是宽限期,一旦设定,您可以期待

  1. 用户可以在注册后自动登录,不再需要重定向到确认页面。

  2. 在此期间,可以记住未经证实的用户,并毫无问题地登录。

  3. 过期后,未经确认的用户将重定向到确认页面,并且必须在继续之前确认密码。

  4. 启用:confirmable后,我担心宽限期对用户最友好。如果您对此不满意,那么使用:confirmable

    几乎没有意义

    关于JSON响应的附注

    我刚刚注意到你在阅读Rich Peck的评论时使用了JSON响应。我建议你不要使用JSON响应,而是使用Ajax / Plain JS表单作为登录表单的简单方法,并期望Devise进行正常的页面重定向。原因是如果使用JSON响应会有太多资源需要更新,例如,csrf_token,cookie,现有授权等。请参阅我关于类似主题的其他答案:https://stackoverflow.com/a/19538974/1721198

答案 3 :(得分:1)

要添加到@gregates答案,请尝试覆盖用户(资源)模型中的方法active_for_authentication?,如下所示:

def active_for_authentication?
  true
end

如果您能够登录,则问题必须与会话控制器有关。确保你没有错过任何明显的东西,比如指定到新会话控制器的路线