【问题标题】:ActionController::InvalidAuthenticityToken in RegistrationsController#createActionController::InvalidAuthenticityToken 在 RegistrationsController#create
【发布时间】:2014-01-19 11:12:02
【问题描述】:

您好,我正在使用 Devise 进行用户身份验证,突然我的新用户注册无法正常工作。

这是我遇到的错误。

ActionController::InvalidAuthenticityToken

Rails.root: /home/example/app
Application Trace | Framework Trace | Full Trace

Request

Parameters:

{"utf8"=>"✓",
 "user"=>{"email"=>"example@gmail.com",
 "password"=>"[FILTERED]",
 "password_confirmation"=>"[FILTERED]"},
 "x"=>"0",
 "y"=>"0"}

这是我的注册控制器

class RegistrationsController < Devise::RegistrationsController
  prepend_before_filter :require_no_authentication, :only => [ :new, :create, :cancel ]
  prepend_before_filter :authenticate_scope!, :only => [:edit, :update, :destroy]

  before_filter :configure_permitted_parameters

  prepend_view_path 'app/views/devise'

  # GET /resource/sign_up
  def new
    build_resource({})
    respond_with self.resource
  end

  # POST /resource
  def create
    build_resource(sign_up_params)

    if resource.save
      if resource.active_for_authentication?
        set_flash_message :notice, :signed_up if is_navigational_format?
        sign_up(resource_name, resource)
        respond_with resource, :location => after_sign_up_path_for(resource)
      else
        set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
        expire_session_data_after_sign_in!
        respond_with resource, :location => after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource

      respond_to do |format|
        format.json { render :json => resource.errors, :status => :unprocessable_entity }
        format.html { respond_with resource }
      end
    end
  end

  # GET /resource/edit
  def edit
    render :edit
  end

  # PUT /resource
  # We need to use a copy of the resource because we don't want to change
  # the current user in place.
  def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
    prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

    if update_resource(resource, account_update_params)
      if is_navigational_format?
        flash_key = update_needs_confirmation?(resource, prev_unconfirmed_email) ?
          :update_needs_confirmation : :updated
        set_flash_message :notice, flash_key
      end
      sign_in resource_name, resource, :bypass => true
      respond_with resource, :location => after_update_path_for(resource)
    else
      clean_up_passwords resource
      respond_with resource
    end
  end

  # DELETE /resource
  def destroy
    resource.destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    set_flash_message :notice, :destroyed if is_navigational_format?
    respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
  end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  def cancel
    expire_session_data_after_sign_in!
    redirect_to new_registration_path(resource_name)
  end

  protected

  # Custom Fields
  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) do |u|
      u.permit(:first_name, :last_name,
        :email, :password, :password_confirmation)
    end
  end

  def update_needs_confirmation?(resource, previous)
    resource.respond_to?(:pending_reconfirmation?) &&
      resource.pending_reconfirmation? &&
      previous != resource.unconfirmed_email
  end

  # By default we want to require a password checks on update.
  # You can overwrite this method in your own RegistrationsController.
  def update_resource(resource, params)
    resource.update_with_password(params)
  end

  # Build a devise resource passing in the session. Useful to move
  # temporary session data to the newly created user.
  def build_resource(hash=nil)
    self.resource = resource_class.new_with_session(hash || {}, session)
  end

  # Signs in a user on sign up. You can overwrite this method in your own
  # RegistrationsController.
  def sign_up(resource_name, resource)
    sign_in(resource_name, resource)
  end

  # The path used after sign up. You need to overwrite this method
  # in your own RegistrationsController.
  def after_sign_up_path_for(resource)
    after_sign_in_path_for(resource)
  end

  # The path used after sign up for inactive accounts. You need to overwrite
  # this method in your own RegistrationsController.
  def after_inactive_sign_up_path_for(resource)
    respond_to?(:root_path) ? root_path : "/"
  end

  # The default url to be used after updating a resource. You need to overwrite
  # this method in your own RegistrationsController.
  def after_update_path_for(resource)
    signed_in_root_path(resource)
  end

  # Authenticates the current scope and gets the current resource from the session.
  def authenticate_scope!
    send(:"authenticate_#{resource_name}!", :force => true)
    self.resource = send(:"current_#{resource_name}")
  end

  def sign_up_params
    devise_parameter_sanitizer.sanitize(:sign_up)
  end

  def account_update_params
    devise_parameter_sanitizer.sanitize(:account_update)
  end
end

这是我的会话控制器

class SessionsController < DeviseController
  prepend_before_filter :require_no_authentication, :only => [ :new, :create ]
  prepend_before_filter :allow_params_authentication!, :only => :create
  prepend_before_filter { request.env["devise.skip_timeout"] = true }

  prepend_view_path 'app/views/devise'

  # GET /resource/sign_in
  def new
    self.resource = resource_class.new(sign_in_params)
    clean_up_passwords(resource)
    respond_with(resource, serialize_options(resource))
  end

  # POST /resource/sign_in
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message(:notice, :signed_in) if is_navigational_format?
    sign_in(resource_name, resource)

    respond_to do |format|
        format.json { render :json => {}, :status => :ok }
        format.html { respond_with resource, :location => after_sign_in_path_for(resource) } 
    end
  end

  # DELETE /resource/sign_out
  def destroy
    redirect_path = after_sign_out_path_for(resource_name)
    signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
    set_flash_message :notice, :signed_out if signed_out && is_navigational_format?

    # We actually need to hardcode this as Rails default responder doesn't
    # support returning empty response on GET request
    respond_to do |format|
      format.all { head :no_content }
      format.any(*navigational_formats) { redirect_to redirect_path }
    end
  end


  protected

  def sign_in_params
    devise_parameter_sanitizer.sanitize(:sign_in)
  end

  def serialize_options(resource)
    methods = resource_class.authentication_keys.dup
    methods = methods.keys if methods.is_a?(Hash)
    methods << :password if resource.respond_to?(:password)
    { :methods => methods, :only => [:password] }
  end

  def auth_options
    { :scope => resource_name, :recall => "#{controller_path}#new" }
  end
end

这是注册表

<%= form_for(:user, :html => {:id => 'register_form'}, :url => user_registration_path, :remote => :true, :format => :json) do |f| %>

    <div class="name_input_container">
        <div class="name_input_cell">


    <%= f.email_field :email, :placeholder => "email" %>


    <%= f.password_field :password, :placeholder => "password", :title => "8+ characters" %>


    <%= f.password_field :password_confirmation, :placeholder => "confirm password" %>


    <div class="option_buttons">
        <div class="already_registered">
            <%= link_to 'already registered?', '#', :class => 'already_registered', :id => 'already_registered', :view => 'login' %>
        </div>
        <%= image_submit_tag('modals/account/register_submit.png', :class => 'go') %>
        <div class="clear"></div>
    </div>
<% end %>

【问题讨论】:

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


    【解决方案1】:

    根据核心application_controller.rb 中的the comments,将protect_from_forgery 设置为以下内容:

    protect_from_forgery with: :null_session
    

    或者,根据the docs,简单地声明protect_from_forgery没有:with参数将默认使用:null_session

    protect_from_forgery # Same as above
    

    更新

    这似乎是设计行为中的documented bug。 Devise suggests disabling protect_from_forgery 的作者关于引发此异常的特定控制器操作:

    # app/controllers/users/registrations_controller.rb
    class RegistrationsController < Devise::RegistrationsController
      skip_before_filter :verify_authenticity_token, :only => :create
    end
    

    【讨论】:

    • 是的,我已经这样做了,但现在我无法在我的终端上发布它显示 Started POST "/users" for 127.0.0.1 at 2014-01-02 04:42:11 +0100 Processing by RegistrationsController#create as HTML 参数:{"utf8"=>"✓", "user"=>{"email"=>"example@gmail.com", "password"=>"[FILTERED]", "password_confirmation" =>"[FILTERED]"}, "commit"=>"Sign up"} 无法验证 CSRF 令牌真实性 (0.6ms) BEGIN (0.6ms) ROLLBACK Completed 200 OK in 18ms (Views: 0.5ms | ActiveRecord: 1.1毫秒)
    • 感谢您的回答。在这一点上提及这是否有任何安全影响以及这是否会在开发与生产中表现不同可能会很有用。
    • @AshStuart,开发与生产中的行为在功能上没有区别。 存在一个安全问题,因为protect_from_forgery 是防御 CSRF 攻击的主要手段。尝试此修复的任何人都可能希望研究替代安全措施,包括 IP 限制。
    • 另外,你需要在你的路由文件中添加这样的东西,以便设计知道你已经覆盖了控制器:devise_for :users, controllers: {registrations: "registrations"}
    • 在尝试之前,请确保您没有忘记 stackoverflow.com/a/24081738/25192 中建议的 csrf_meta_tags
    【解决方案2】:

    您忘记在布局文件中添加&lt;%= csrf_meta_tags %&gt;

    例如:

    <!DOCTYPE html>
    <html>
    <head>
    <title>Sample</title>
    <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
    <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
    <%= csrf_meta_tags %>
    </head>
    <body>
    
    <%= yield %>
    
    </body>
    </html>
    

    【讨论】:

    • 如果你看到这个问题并且你的布局中已经有csrf_meta_tags,这可能是由于protect_from_forgery钩子被调用的顺序——试着确保它是第一个声明的钩子在您的ApplicationController 中(作为免责声明,这对我使用 Rails 5.1.3 有效)
    • 感谢@lshepstone 的解决方案!这个问题开始随机弹出,想不通!
    【解决方案3】:

    TLDR:您可能会看到此问题,因为您的表单是通过 XHR 提交的。

    先说几件事:

    1. Rails 在页面的 head 标签内包含一个 CSRF 令牌。
    2. Rails 会在您执行 POST、PATCH 或 DELETE 请求时评估此 CSRF 令牌。
    3. 此令牌在您登录或退出时过期

    沼泽标准 HTTP 登录将导致整个页面刷新,并且旧的 CSRF 令牌将刷新替换 Rails 创建的全新令牌你登录。

    AJAX 登录将不会刷新页面,因此现在无效的陈旧、陈旧的 CSRF 令牌仍然存在于您的页面上。

    解决方案是在 AJAX 登录后手动更新 HEAD 标签内的 CSRF 令牌。


    一些步骤是我无耻地从有帮助的thread on this matter 那里借来的。

    第 1 步: 将新的 CSRF-token 添加到成功登录后发送的响应标头中

    class SessionsController < Devise::SessionsController
    
      after_action :set_csrf_headers, only: :create
    
      # ...
    
      protected
        def set_csrf_headers
          if request.xhr?
            # Add the newly created csrf token to the page headers
            # These values are sent on 1 request only
            response.headers['X-CSRF-Token'] = "#{form_authenticity_token}"
            response.headers['X-CSRF-Param'] = "#{request_forgery_protection_token}"
          end
        end
      end
    

    第二步:ajaxComplete事件触发时使用jQuery更新页面:

    $(document).on("ajaxComplete", function(event, xhr, settings) {
      var csrf_param = xhr.getResponseHeader('X-CSRF-Param');
      var csrf_token = xhr.getResponseHeader('X-CSRF-Token');
    
      if (csrf_param) {
        $('meta[name="csrf-param"]').attr('content', csrf_param);
      }
      if (csrf_token) {
        $('meta[name="csrf-token"]').attr('content', csrf_token);
      }
    });
    

    就是这样。 YMMV 取决于您的设计配置。我怀疑这个问题最终是由于旧的 CSRF 令牌正在终止请求,并且 rails 抛出异常。

    【讨论】:

      【解决方案4】:

      如果您只使用 API,您应该尝试:

      class ApplicationController < ActionController::Base
        protect_from_forgery unless: -> { request.format.json? }
      end
      

      http://edgeapi.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html#method-i-protect_against_forgery-3F

      【讨论】:

      • 添加这个,我现在得到一个 406 错误:xhr.js:173 POST http://localhost:3000/users/sign_in.json 406 (Not Acceptable)ActionController::UnknownFormat in Devise::SessionsController#create
      • 更新:然后我必须在我的应用程序控制器中添加:respond_to :html, :json。现在它工作正常!
      【解决方案5】:

      对于 Rails 5,这可能是由于触发 protect_from_forgery 和您的 before_actions 的顺序。

      我最近遇到了类似的情况,尽管protect_from_forgery with: :exceptionApplicationController 中的第一行,但before_action 仍然在干扰。

      解决办法是改变:

      protect_from_forgery with: :exception
      

      到:

      protect_from_forgery prepend: true, with: :exception
      

      这里有一篇关于它的博客文章http://blog.bigbinary.com/2016/04/06/rails-5-default-protect-from-forgery-prepend-false.html

      【讨论】:

        【解决方案6】:

        整个上午都在调试这个,所以我想我应该在这里分享一下,以防有人在将 rails 更新到 5.2 或 6 时遇到类似问题。

        我有两个问题

        1) 无法验证 CSRF 令牌的真实性。

        并且,在添加跳过验证之后,

        2) 请求将通过,但用户仍未登录。

        我没有在开发中缓存

          if Rails.root.join('tmp', 'caching-dev.txt').exist?
            config.action_controller.perform_caching = true
            config.action_controller.enable_fragment_cache_logging = true
        
            config.cache_store = :memory_store
            config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" }
          else
            config.action_controller.perform_caching = false
        
            config.cache_store = :null_store
          end
        

        在 session_store 中

        config.session_store :cache_store,  servers: ... 
    
    
        

        我猜应用程序试图将会话存储在缓存中,但它是空的 - 所以它没有登录。在我运行之后

        bin/rails dev:cache
        

        其中开始缓存 - 登录开始工作。

        你可能需要

        • 旋转master.key
        • 轮换 credentials.yml.enc
        • 删除 secrets.yml

        【讨论】:

        • 在我的情况下,只需打开缓存即可解决问题,无需任何验证跳过或自定义protect_from_forgery 行(Rails 6)
        • 谢谢!我有同样的问题,缓存禁用不适用于缓存 cookie 存储...
        【解决方案7】:

        浏览器缓存 HTML 问题 (2020)

        如果您已尝试此页面上的所有补救措施,但仍然遇到InvalidAuthenticityToken 异常问题,则可能与浏览器缓存 HTML 有关。 an issue on Github 有 100 个 cmets 以及一些可重现的代码。简而言之,这就是发生在我身上的与 HTML 缓存有关的事情:

        1. 用户浏览网站。 Rails 在第一个 GET 请求上设置一个签名会话 cookie。有关配置选项,请参阅 config/initializers/session_store.rb。此会话 cookie 存储有用的信息,包括用于解密和验证请求真实性的 CSRF 令牌。重要提示:默认情况下,会话 cookie 将在浏览器窗口关闭时过期。
        2. 用户浏览到包含表单的页面。对我来说,我在登录页面上收到的异常最多。
        3. Rails 在此表单中嵌入了一个隐藏的 CSRF 令牌,并将此令牌与表单数据一起提交。重要提示:此令牌嵌入在 HTML 中。
        4. ActionController 从 params 对象中获取 CSRF 令牌,并使用 Rails 4.2+ 中的 verified_request? 方法使用 cookie 中的 CSRF 令牌对其进行验证。

        许多浏览器现在都实现了 HTML 缓存,因此当您打开页面时,无需请求即可加载 HTML。不幸的是,当浏览器关闭时,会话 cookie 被破坏,所以如果用户在表单(例如登录页面)上关闭浏览器,那么第一个请求将不包含 CSRF 令牌,从而引发 InvalidAuthenticityError。

        两种常见的解决方案

        1. 将会话 cookie 的有效期延长到浏览器窗口之外。
        2. 在浏览器中检测会话 cookie 是否丢失(通过代理 cookie),如果丢失则刷新页面。

        1。延长会话 cookie 的有效期

        正如Github comment 中所述,Django 采用这种方法:

        Django puts 将令牌添加到它自己的名为 CSRF_COOKIE 的 cookie 中。这是一个在一年内到期的持久性 cookie。如果发出后续请求,则更新 cookie 的过期时间。

        在 Rails 中:

        # config/initializers/session_store.rb 
        Rails.application.config.session_store :cookie_store, expire_after: 14.days
        

        由于许多与安全相关的事情,concern 表示这可能会造成漏洞,但我无法找到任何攻击者如何利用此漏洞的示例。

        2。使用javascript刷新页面

        此方法涉及设置一个可由浏览器读取的单独令牌,如果该令牌不存在,则刷新页面。因此,当浏览器加载缓存的 HTML(没有会话 cookie),在页面上执行 JS 时,用户可以被重定向或刷新 HTML。

        例如为每个不受保护的请求设置一个cookie:

        # app/controllers/application_controller.rb
        class ApplicationController < ActionController::Base
          after_action :set_csrf_token
        
          def set_csrf_token
            cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
          end
        end
        

        在 JS 中检查这个 cookie:

        const hasCrossSiteReferenceToken = () => document.cookie.indexOf('XSRF-TOKEN') > -1;
        
        if (!hasCrossSiteReferenceToken()) {
            location.reload();
        }
        

        这将强制浏览器刷新。

        结论

        我希望这对那里的一些人有所帮助;这个错误花费了我几天的工作时间。如果您仍有问题,请考虑继续阅读:

        【讨论】:

          【解决方案8】:

          您必须将protect_from_forgery 放在验证用户的操作之前。这是正确的解决方案

          class ApplicationController < ActionController::Base
            protect_from_forgery with: :exception
            before_action :authenticate_user!
          end
          

          【讨论】:

          • @Vadim 你知道吗?
          • 不知道,@ChrisEdwards。这就是我最终做的事情:skip_before_action :verify_authenticity_token, if: -&gt; { controller_name == 'sessions' }。它现在允许我登录和注销。以前,我无法使用这两种操作。当然,这并不理想,而且有点安全问题。不是解决方案 - 只是一种解决方法。
          • @Vadim 和你描述的完全一样,谢谢。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-07-15
          • 2018-11-02
          • 2015-07-20
          • 2011-03-22
          • 2018-11-27
          • 2016-03-24
          • 2015-11-23
          相关资源
          最近更新 更多