【问题标题】:Rails Devise Password Reset Email allowing multiple submissionsRails 设计密码重置电子邮件,允许多次提交
【发布时间】:2013-09-15 17:51:43
【问题描述】:

我有以下代码允许用户以 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 %>

这允许这样的行为,即如果用户反复单击按钮,或反复按“输入”,则在服务器可以提供响应之前,将发送相应的 # 封密码重置电子邮件。

以下内容在 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

有没有办法只回复第一次提交?

谢谢

【问题讨论】:

    标签: ruby-on-rails ruby ajax devise password-recovery


    【解决方案1】:

    我建议使用 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 的回答

    我浏览了设计源,发现应该有模型级别的解决方案。要设置每个重置请求之间允许的最大间隔,请在资源模型中添加此类

    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 =&gt;'true' 不再通过 AJAX 提交表单。
    • @Elliott, remote: true 应该找到 IMO。这是否会阻止您禁用表单?
    • 没有它,我相信表单只是通过普通浏览器协议提交的——我相信这可以防止一次多次提交。一旦我弄清楚我想要显示的视图告诉用户一封电子邮件已发送,我计划实施你的建议。就目前而言,我所做的只是刷新页面并显示通知。
    • @Ellitt,明白了,有利于逐步改进。你也可以考虑我的更新。
    • 关于“重置密码”的这一点现在不在 API 中,也不在源代码中,因此下面基于控制器的答案似乎更好。不过,将类似的内容添加到设计模块可能会很好,因为用户在请求 3 次并单击他们收到的第一个(现在无效)时链接更改会真的感到困惑。
    【解决方案2】:

    如果您真的只是想阻止人们双击提交,那么正如 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 错误可能会让用户感到惊讶,因为他们可能不知道他们已经完成了多次提交。从安全的角度来看,我仍然喜欢你的建议,非常巧妙 - 稍后会尝试。
    • @trh,好主意,但代码有点乱。我刚刚浏览了设计代码,发现有一个模型级解决方案不需要控制器上的额外代码。检查我的答案更新。
    【解决方案3】:

    我认为如果您要与客户打交道,这个想法非常有用,他们不会等待电子邮件而是重新请求 3 或 4 次,此时第一个可能会出现,但现在会有无效的链接。滞后或只是重新发送相同的链接很不错,但正如我上面提到的,它不再 (?) 在 devise code 中,它只处理过期的旧重置请求,而不是限制新请求的发送。

    我采用了 trh 想法的简化版本,它有选择地转发到原始设计代码。如果在过去一小时内发送了一个请求,它只是假装它再次发送了它,并假设 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
    

    【讨论】:

    • github.com/plataformatec/devise/blob/… 覆盖 send_devise_notification 以创建邮件队列也可能很有趣
    • if 条件应该是:if resource &amp;&amp; (resource.reset_password_sent_at.nil? || Time.now &gt; resource.reset_password_sent_at + 1.hour)
    【解决方案4】:

    你可以在 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 是允许的密码重置间隔。

    【讨论】:

      猜你喜欢
      • 2017-02-22
      • 1970-01-01
      • 2011-09-20
      • 1970-01-01
      • 2018-10-07
      • 2017-08-30
      • 2014-12-21
      • 1970-01-01
      • 2023-04-04
      相关资源
      最近更新 更多