Rails Devise密码重置电子邮件允许多次提交

4
我有以下代码,可以让用户在AJAX表单中请求重置密码:
<%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post },:remote =>'true') do |f| %>
 <%= devise_error_messages! %>
 <div><%= f.label :email %><br />    
 <%= f.email_field :email %></div>
 <div><%= f.submit "Send me reset password instructions" %></div>
<% end %>

这会导致一个行为,即如果用户在服务器提供响应之前重复点击按钮或重复按下“enter”键,则将发送相应数量的密码重置电子邮件。

以下内容位于devise/password_controller.rb中。

def create
 self.resource = resource_class.send_reset_password_instructions(resource_params)   
 if successfully_sent?(resource)
  flash[:notice] = "You will receive an email with instructions about how to reset your password in a few minutes."
  respond_to do |format|
   format.html #responds with default html file
   format.js 
  end    
 else
  respond_to do |format|
   format.html #responds with default html file
   format.js{ render :js => "$(\".deviseErrors\").html(\"<span class='login-error'>Could not send reset instructions to that address.</span>\");" } #this will be the javascript file we respond with
  end
 end
end

有没有一种方法只回复第一次提交?
谢谢。
4个回答

4

如果您与客户打交道,他们可能不想等待电子邮件,而会重新请求3或4次,此时第一封邮件可能已经到达,但链接已失效。具有滞后特性或仅重新发送相同链接是很好的选择,但如上所述,这似乎不再在devise代码中处理,它只处理过期的重置请求,而不限制新请求的发送。

我使用了trh的想法的简化版本,有选择地转发到原始的devise代码。如果在最近一个小时内已发送请求,则假装又发送了一遍,并且假设Mailgun或您正在使用的其他服务将把消息传递给目标收件人。

class Members::PasswordsController < Devise::PasswordsController
  def create
    self.resource = resource_class.find_by_email(resource_params[:email])
    if resource && (!resource.reset_password_sent_at.nil? || Time.now > resource.reset_password_sent_at + 1.hour)
      super
    else
      flash[:notice] = I18n.t('devise.passwords.send_instructions')
      respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
    end
  end
end

表现如下:

  specify "asking twice sends the email once only, until 1 hour later" do
    member = make_activated_member
    ActionMailer::Base.deliveries.clear
    2.times do
      ensure_on member_dashboard_path
      click_on "Forgotten your password?"
      fill_in "Email", :with => member.email
      click_on "Send me password reset instructions"
    end
    # see for mail helpers https://github.com/bmabey/email-spec/blob/master/lib/email_spec/helpers.rb

    expect(mailbox_for(member.email).length).to eq(1)
    expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))    

    Timecop.travel(Time.now + 2.hours) do
      expect {
        ensure_on member_dashboard_path
        click_on "Forgotten your password?"
        fill_in "Email", :with => member.email
        click_on "Send me password reset instructions"
      }.to change{mailbox_for(member.email).length}.by(+1)
    end
  end

奖励分数可以用来更新并重新发送带有相同链接的原始电子邮件,就像这个测试一样:
  specify "asking twice sends the same link both times" do
    member = make_activated_member
    ActionMailer::Base.deliveries.clear
    2.times do
      visit member_dashboard_path
      click_on "Forgotten your password?"
      fill_in "Email", :with => member.email
      click_on "Send me password reset instructions"
    end
    # see for mail helpers https://github.com/bmabey/email-spec/blob/master/lib/email_spec/helpers.rb

    mails = mailbox_for(member.email)
    expect(mails.length).to eq(2)
    first_mail = mails.first
    second_mail = mails.last

    expect(links_in_email(first_mail)).to eq(links_in_email(second_mail))
  end

https://github.com/plataformatec/devise/blob/3d9dea39b2978e3168604ccda956fb6ec17c5e27/lib/devise/models/authenticatable.rb#L135 覆盖 send_devise_notification 方法以创建邮件队列可能也是有趣的。 - nruth
if条件应该是: if resource && (resource.reset_password_sent_at.nil? || Time.now > resource.reset_password_sent_at + 1.hour) - Karens

3
我建议使用JavaScript来防止多次提交。
$('form#reset_password').on('submit', function() {
  $(this).find('input[type="submit"]').attr('disabled', 'disabled')
})

这将把提交按钮设置为“禁用”状态,用户不能再次提交。
引用有关表单的禁用属性的参考:http://www.w3schools.com/tags/att_input_disabled.asp*
添加:响应thr的答案
我浏览了Devise源代码,并发现在模型层应该有一种解决方案。要设置每个重置请求之间允许的最大时间间隔,请在资源模型中添加此类内容。
class User < ActiveRecord::Base

  def self.reset_password_with
    1.day
    # Determine the interval. Any time objects will do, say 1.hour
  end
end

然后 Devise::Models::Recoverable 将检查此值以决定是否应发送令牌。我没有验证过,但它应该可以工作。


谢谢,这很简单。最后我删除了 :remote =>'true',不再通过 AJAX 提交表单。 - Elliott de Launay
@ElliottпјҢжҲ‘и®Өдёәremote: trueеә”иҜҘжҳҜеҸҜд»Ҙзҡ„гҖӮиҝҷдјҡйҳІжӯўжӮЁзҰҒз”ЁиЎЁеҚ•еҗ—пјҹ - Billy Chan
没有它,我相信表单只是通过正常的浏览器协议提交 - 这防止了同时多次提交。一旦我想出要显示告诉用户已发送电子邮件的视图,我就打算实施您的建议。目前,我只是刷新页面并显示通知。 - Elliott de Launay
@Ellitt,明白了,逐步改进不错。你也可以考虑我的更新。 - Billy Chan
关于“reset password with”的内容目前既不在API中,也不在源代码中,因此似乎下方基于控制器的答案更好。如果将类似这样的内容添加到设备模块中,那就太好了,因为当用户请求3次并单击他们接收到的第一个链接(现在已无效)时,他们会变得非常困惑。 - nruth
reset_password_within 但它定义的是令牌的有效期限,而不是你可以重置密码的频率。 - Maxim Krizhanovsky

2
您可以在Devise中这样做:
class User < ActiveRecord::Base
  def send_reset_password_instructions
    super unless reset_password_sent_at.present? && reset_password_sent_at > DateTime.now - 1.day
  end
end

1.day 是允许密码重置的时间间隔。


1
如果你只是想防止用户重复点击提交按钮,那么像billy-chan在他的回答中建议的那样,通过JavaScript限制是可行的。
如果你想限制向给定用户发送请求的时间间隔,那么可以设置资源,并将该功能包装在if语句中,检查上次密码请求发送的时间戳。类似这样的代码:
def create
  self.resource = resource_class.find_by_email(resource_params[:email])
  if resource.reset_password_sent_at.nil?  ||  Time.now > resource.reset_password_sent_at + 5.minutes
    self.resource = resource_class.send_reset_password_instructions(resource_params)
    if successfully_sent?(resource)
      flash[:notice] = "You will receive an email with instructions about how to reset your password in a few minutes."
      respond_to do |format|
        format.html #responds with default html file
        format.js
      end
    else
      respond_to do |format|
        format.html #responds with default html file
        format.js{ render :js => "$(\".deviseErrors\").html(\"<span class='login-error'>Could not send reset instructions to that address.</span>\");" } #this will be the javascript file we respond with
      end
    end
  else
    flash[:error] = "Passwords can only be reset every 5 minutes."
    respond_to do |format|
      format.html #responds with default html file
      format.js
    end
  end
end

谢谢trh,将其包装在if语句中的唯一问题是Flash错误可能会让用户感到惊讶,因为他们可能不知道自己已经提交了多个请求。从安全角度来看,我仍然喜欢您的建议,非常巧妙-稍后会尝试这个方法。 - Elliott de Launay
@trh,想法不错,但代码有点乱。我刚刚浏览了Devise的代码,并发现有一种模型级别的解决方案,不需要在控制器上添加额外的代码。请查看我的答案更新。 - Billy Chan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接