【问题标题】:Devise log after auth failure验证失败后设计日志
【发布时间】:2012-10-13 14:17:25
【问题描述】:

当有人无法登录我的应用程序时,我需要写一个日志(以跟踪暴力破解尝试)。我还决定记录成功的身份验证。 所以我创建了一个 SessionsController 并尝试像这样覆盖 session#create 方法:https://gist.github.com/3884693

第一部分工作得很好,但是当身份验证失败时,rails 会抛出某种异常并且永远不会到达 if 语句。所以我不知道该怎么办。

【问题讨论】:

    标签: authentication devise ruby-on-rails-3.2


    【解决方案1】:

    This answer to a previous SO question - Devise: Registering log in attempts 有答案。

    设计控制器中的创建操作调用warden.authenticate!,它尝试使用提供的参数对用户进行身份验证。如果身份验证失败,则进行身份验证!将调用设计失败应用程序,然后运行 ​​SessionsController#new 操作。请注意,如果身份验证失败,您为创建操作设置的任何过滤器都不会运行。

    因此解决方案是在新操作之后添加一个过滤器,该过滤器检查 env["warden.options"] 的内容并采取适当的操作。

    我尝试了该建议,并且能够记录成功和失败的登录尝试。下面是相关的控制器代码:

    class SessionsController < Devise::SessionsController
      after_filter :log_failed_login, :only => :new
    
      def create
        super
        ::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
      end
    
      private
      def log_failed_login
        ::Rails.logger.info "\n***\nFailed login with email_id : #{request.filtered_parameters["user"]}\n***\n" if failed_login?
      end 
    
      def failed_login?
        (options = env["warden.options"]) && options[:action] == "unauthenticated"
      end 
    end
    

    日志有以下条目:

    成功登录

    Started POST "/users/sign_in"
    ...
    ...
    ***
    Successful login with email_id : {"email"=>...
    ***
    ...
    ...
    Completed 302 Found
    

    登录失败

    Started POST "/users/sign_in"
    ...
    ...
    Completed 401 Unauthorized 
    Processing by SessionsController#new as HTML
    ...
    ...
    ***
    Failed login with email_id : {"email"=>...
    ***
    ...
    ...
    Completed 302 Found
    

    【讨论】:

    • 我还按照 Configuring Controllers 上的设计指南让其余代码正常工作。
    • 我不得不把它改成一个符号:options[:action] == :unauthenticated
    • 是否还有一种方法可以捕获由于用户尝试使用未经确认的帐户登录而导致的失败登录尝试?
    【解决方案2】:

    Prakash's answer 很有帮助,但依赖SessionsController#new 作为副作用运行并不理想。我相信这更干净:

    class LogAuthenticationFailure < Devise::FailureApp
      def respond
        if request.env.dig('warden.options', :action) == 'unauthenticated'
          Rails.logger.info('...')
        end
        super
      end
    end
    
    ...
    
    Devise.setup do |config|
    
    config.warden do |manager|
      manager.failure_app = LogAuthenticationFailure
    end
    

    如果您希望挂钩 Warden 的回调,请查看 Graeme's answer(使用 Warden 实现设计)。

    【讨论】:

    • 使用这种方法需要考虑的一点是,它还会记录访问未经身份验证的路由...您可以添加另一个过滤器以确保它仅用于登录尝试:if env.dig("warden.options", :action) == "unauthenticated" &amp;&amp; env.dig("warden.options", :message) == :invalid
    • 在 Rails 5.1 中,我收到了 DEPRECATION WARNING: env is deprecated and will be removed from Rails 5.1
    • @Purplejacket 感谢您的评论!我根据stackoverflow.com/a/34471522/1067145env 更新为request.env。如果您仍然看到弃用或有任何其他问题,请告诉我。
    • 是否有任何参数传递给Devise::FailureApp。换句话说,我是否能够从尝试登录的用户中提取一些信息?
    • @bo-oz 我已经很久没有使用这个代码了(无法验证这个),但是尝试挖掘一下request.env?您应该能够获取会话信息。
    【解决方案3】:

    我有同样的问题,但无法使用 "warden.options" 解决它,因为在我的情况下,这些问题在重定向到 sessions#new 操作之前已被清除。在研究了一些我认为太脆弱的替代方案(因为它们涉及扩展一些 Devise 类和对现有方法进行别名)之后,我最终使用了一些 callbacks provided by Warden。它对我来说效果更好,因为回调是在当前请求-响应周期内调用的,并且参数都保存在 env 对象中。

    这些回调被命名并且似乎旨在解决此问题和相关问题。并且记录在案!

    Warden 从warden-1.2.3 开始支持以下回调:

    • after_set_user
    • after_authentication(用于记录成功登录)
    • after_fetchafter_set_user 的别名)
    • before_failure(用于记录失败的登录 - 示例如下)
    • after_failed_fetch
    • before_logout
    • on_request

    每个回调都直接在Warden::Manager 类上设置。为了跟踪失败的身份验证尝试,我添加了以下内容:

    Warden::Manager.before_failure do |env, opts|
      email = env["action_dispatch.request.request_parameters"][:user] &&
              env["action_dispatch.request.request_parameters"][:user][:email]
      # unfortunately, the User object has been lost by the time 
      # we get here; so we take a db hit because I care to see 
      # if the email matched a user account in our system
      user_exists = User.where(email: email).exists?
    
      if opts[:message] == :unconfirmed
        # this is a special case for me because I'm using :confirmable
        # the login was correct, but the user hasn't confirmed their 
        # email address yet
        ::Rails.logger.info "*** Login Failure: unconfirmed account access: #{email}"
      elsif opts[:action] == "unauthenticated"
        # "unauthenticated" indicates a login failure
        if !user_exists
          # bad email:
          # no user found by this email address
          ::Rails.logger.info "*** Login Failure: bad email address given: #{email}"
        else
          # the user exists in the db, must have been a bad password
          ::Rails.logger.info "*** Login Failure: email-password mismatch: #{email}"
        end
      end
    end
    

    我希望您也可以使用before_logout 回调来跟踪注销操作,但我还没有测试过。回调似乎也有prepend_ 变体。

    【讨论】:

    • 似乎不再需要额外的数据库命中。当在数据库中找不到用户时,opts[:message] 设置为:not_found_in_database
    【解决方案4】:

    对于注销日志,您需要捕获销毁事件,因此将以下内容添加到 Session 控制器(来自上述答案):

    before_filter :log_logout, :only => :destroy  #add this at the top with the other filters
    
    def log_logout
         ::Rails.logger.info "*** Logging out : #{current_user.email} ***\n"  
    end
    

    【讨论】:

      【解决方案5】:

      我找到了另一种方法来执行此操作,例如,如果您想在登录失败时显示自定义消息。

      在我的工作中,如果登录失败,我们会检查活动状态(自定义逻辑)并显示一条消息,无论登录是否正确。

      经过一点调试并阅读warden 文档后,我现在知道了:Warden 执行throw(:warden, opts),因此,根据 ruby​​ 文档,throw 必须在catch 块内捕获。

      def create
        flash.clear
        login_result = catch(:warden) { super }
        return unless login_failed?(login_result)
      
        email = params[:user][:email]
        flash[:alert] = # here I call my service that calculates the message
        redirect_to new_user_session_path
      end
      
      def login_failed?(login_result)
        login_result.is_a?(Hash) && login_result.key?(:scope) && login_result.key?(:recall)
      end
      

      抛出文档: https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-throw

      捕获文档: https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-catch

      【讨论】:

        【解决方案6】:

        基于 Prakash Murty 的回答,我认为这个答案 (https://stackoverflow.com/a/34816998/891359) 中的方法是记录成功登录尝试的一种更简洁的方法。 Devise 提供了一种在视图渲染之前传递 yielded 块的方法,而不是调用 super。

        所以不要这样做:

        class SessionsController < Devise::SessionsController
          def create
            super
            ::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
          end
        end
        

        这样做更干净:

        class SessionsController < Devise::SessionsController
          def create
            super do |user|
              ::Rails.logger.info "\n***\nSuccessful login with email_id : #{user.email}\n***\n"
            end
          end
        end
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-07-09
          • 1970-01-01
          • 1970-01-01
          • 2019-06-26
          • 2013-01-07
          • 2016-10-14
          • 1970-01-01
          • 2020-08-28
          相关资源
          最近更新 更多