Ruby-on-rails ActionController::InvalidAuthenticityToken in RegistrationsController#create

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20875591/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 23:11:02  来源:igfitidea点击:

ActionController::InvalidAuthenticityToken in RegistrationsController#create

ruby-on-railsdeviseruby-on-rails-4

提问by user3144005

Hi I am using Devise for my user authentication suddenly my new user registration was not working.

嗨,我正在使用 Devise 进行用户身份验证,突然我的新用户注册不起作用。

this was error I am getting.

这是我得到的错误。

ActionController::InvalidAuthenticityToken

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

Request

Parameters:

{"utf8"=>"?",
 "user"=>{"email"=>"[email protected]",
 "password"=>"[FILTERED]",
 "password_confirmation"=>"[FILTERED]"},
 "x"=>"0",
 "y"=>"0"}

this is my registrations controller

这是我的注册控制器

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

and this is my sessions controller

这是我的会话控制器

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

this is registration form

这是注册表

<%= 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 %>

回答by zeantsoi

Per the commentsin the core application_controller.rb, set protect_from_forgeryto the following:

根据core 中的注释application_controller.rb,设置protect_from_forgery为以下内容:

protect_from_forgery with: :null_session

Alternatively, per the docs, simply declaring protect_from_forgerywithout a :withargumentwill utilize :null_sessionby default:

或者,根据docs,简单地声明protect_from_forgery不带:with参数:null_session默认使用:

protect_from_forgery # Same as above

UPDATE:

更新

This seems to be a documented bugin the behavior of Devise. The author of Devise suggests disabling protect_from_forgeryon the particular controller action that's raising this exception:

这似乎是Devise 行为中记录的错误。Devise 的作者建议禁用protect_from_forgery引发此异常的特定控制器操作:

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

回答by Snm Maurya

You have forgot to add <%= csrf_meta_tags %>in side your layout file.

您忘记<%= csrf_meta_tags %>在布局文件中添加。

e.g.:

例如:

<!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>

回答by stephenmurdoch

TLDR:You are probably seeing this issue because your form submits via XHR.

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

Few things first:

先说几点:

  1. Rails includes a CSRF token inside the head tag of your page.
  2. Rails evaluates this CSRF token anytime you perform a POST, PATCH or DELETE request.
  3. This token expireswhen you sign in or sign out
  1. Rails 在页面的 head 标签中包含一个 CSRF 令牌。
  2. 每当您执行 POST、PATCH 或 DELETE 请求时,Rails 都会评估这个 CSRF 令牌。
  3. 此令牌到期时,您登录或登出

A bog standard HTTP sign-in will cause a full page refresh, and the old CSRF token will be flushedand replacedwith the brand new one that Rails creates when you sign in.

Bog 标准的 HTTP 登录将导致整个页面刷新,旧的 CSRF 令牌将被刷新替换为 Rails 在您登录时创建的全新令牌。

An AJAX sign in will notrefresh the page, so the crusty old, stale CSRF token, which is now invalid, is still present on your page.

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

The solution is to update the CSRF token inside your HEAD tag manually after AJAX sign in.

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



Some steps that I have shamelessly borrowed from a helpful thread on this matter.

我从关于这个问题的有用线程中无耻地借用了一些步骤。

Step 1:Add the new CSRF-token to the response headers which are sent after a successful sign in

第 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

Step2:Use jQuery to update the page with the new values when the ajaxCompleteevent fires:

步骤 2: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);
  }
});

That's it. YMMV depending on your Devise configuration. I suspect though that this issue is ultimately caused by the fact that the old CSRF token is killing the request, and rails throws an exception.

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

回答by Franzé Jr.

If you're using just an API you should try:

如果您只使用 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

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

回答by Chris Edwards

For Rails 5 it could be due to the order in which protect_from_forgeryand your before_actionsare triggered.

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

I faced a similar situation recently, even though protect_from_forgery with: :exceptionwas the first line in the ApplicationController, the before_action's were still interfering.

最近,我遇到了类似的情况,即使protect_from_forgery with: :exception是在第一线ApplicationControllerbefore_action的仍然干扰。

The solution was to change:

解决方案是改变:

protect_from_forgery with: :exception

to:

到:

protect_from_forgery prepend: true, with: :exception

There's a blog post about it here http://blog.bigbinary.com/2016/04/06/rails-5-default-protect-from-forgery-prepend-false.html

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

回答by Richard Lapi?

You have to put protect_from_forgery right before the action for authenticating user. This is the right solution

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

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

回答by Matt

Browser Caching HTML Issue (2020)

浏览器缓存 HTML 问题(2020)

If you've tried all the remedies on this page and you're still having an issue with InvalidAuthenticityTokenexceptions, it may be related to the browser caching HTML. There's an issue on Githubwith 100s of comments along with some reproducible code. In a nutshell, here's what was happening to me as it relates to HTML caching:

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

  1. User browses to website. Rails sets a signed session cookie on the first GET request. See config/initializers/session_store.rbfor config options. This session cookie stores useful information, including a CSRF token that is used to decrypt and validate the authenticity of the request. Important: By default, the session cookie will expire when the browser window closes.
  2. User browses to a page containing a form. For me, I was receiving the most exceptions on my login page.
  3. Rails embeds a hidden CSRF token in this form, and submits this token along with the form data. Important: This token is embedded in the HTML.
  4. ActionController grabs the CSRF token from the params object and validates it with the CSRF token from the cookie using the verified_request?method in Rails 4.2+.
  1. 用户浏览网站。Rails 在第一个 GET 请求上设置一个签名的会话 cookie。请参阅config/initializers/session_store.rb配置选项。此会话 cookie 存储有用的信息,包括用于解密和验证请求真实性的 CSRF 令牌。重要提示:默认情况下,会话 cookie 将在浏览器窗口关闭时过期。
  2. 用户浏览到包含表单的页面。对我来说,我在登录页面上收到的异常最多。
  3. Rails 在此表单中嵌入了一个隐藏的 CSRF 令牌,并将此令牌与表单数据一起提交。重要提示:此标记嵌入在 HTML 中。
  4. ActionController 从 params 对象中获取 CSRF 令牌,并使用verified_request?Rails 4.2+ 中的方法使用 cookie 中的 CSRF 令牌对其进行验证。

Many browsers are now implementing HTML caching, so that when you open a page the HTML is loaded without a request. Unfortunately, when the browser is closed the session cookie is destroyed, so if the user closes the browser while on a form (such as a login page), then the first request will not contain a CSRF token thus throwing an InvalidAuthenticityError.

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

Two common solutions

两种常见的解决方案

  1. Extend the expiry of your session cookie beyond the browser window.
  2. Detect in the browser if the session cookie is missing (via a proxy cookie), and if it is missing refresh the page.
  1. 将会话 cookie 的到期时间延长到浏览器窗口之外。
  2. 在浏览器中检测会话 cookie 是否丢失(通过代理 cookie),如果丢失则刷新页面。

1. Extending the session cookie expiry

1. 延长会话cookie的有效期

As noted in this Github comment, Django takes this approach:

正如Github 评论中所指出的,Django 采用这种方法:

Django puts adds the token in its own cookie called CSRF_COOKIE. This is a persistent cookie that expires in a year. If subsequent requests are made, the cookie's expiry is updated.

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

In Rails:

在导轨中:

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

With many things security related, there's concernthat this could create vulnerabilities, but I have not been able to locate any examples of how an attacker could exploit this.

由于许多与安全相关的事情,有人担心这可能会产生漏洞,但我无法找到攻击者如何利用此漏洞的任何示例。

2. Using javascript to refresh a page

2. 使用javascript刷新页面

This approach involves setting a separate token that can be read by the browser, and if that token is not present, refreshing the page. Thus, when the browser loads the cached HTML (without the session cookie), executes the JS on the page, the user can be redirected or refresh the HTML.

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

For example, setting a cookie for each non-protected request:

例如,为每个不受保护的请求设置一个 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

Checking for this cookie in JS:

在 JS 中检查这个 cookie:

const hasCrossSiteReferenceToken = () => document.cookie.indexOf('XSRF-TOKEN') > -1;

if (!hasCrossSiteReferenceToken()) {
    location.reload();
}

This will force the browser to refresh.

这将强制浏览器刷新。

Conclusion

结论

I hope this helps some folks out there; this bug cost me days of work. If you're still having issues, consider reading up on:

我希望这可以帮助那里的一些人;这个错误花费了我几天的工作时间。如果您仍然遇到问题,请考虑阅读以下内容:

回答by AndreiMotinga

Just spent the entire morning debugging this, so I thought I should share this here in case someone faces a similar issue when updating rails to 5.2 or 6.

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

I had 2 problems

我有两个问题

1) Can't verify CSRF token authenticity.

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

and, after added skipping verification,

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

2) request would go through but user still wasn't logged in.

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

I wasn't caching in development

我没有在开发中缓存

  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

And in session_store

在 session_store 中

config.session_store :cache_store,  servers: ... ?    ?    

I guess app was trying to store session in cache, but it was null - so it wasn't logging in. ??after I ran

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

bin/rails dev:cache

which started caching - login started to work.

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

You may need to

你可能需要

  • Rotate master.key
  • Rotate credentials.yml.enc
  • remove secrets.yml
  • 轮换master.key
  • 轮换凭证.yml.enc
  • 删除 secrets.yml