ActionController::InvalidAuthenticityToken в RegistrationsController# создать

Привет, я использую 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 %>

6 ответов

Решение

За комментарии в ядре application_controller.rb, задавать protect_from_forgery к следующему:

protect_from_forgery with: :null_session

Кроме того, согласно документам, просто объявив protect_from_forgery без :with аргумент будет использовать :null_session по умолчанию:

protect_from_forgery # Same as above

ОБНОВЛЕНИЕ:

Это похоже на задокументированную ошибку в поведении Devise. Автор Devise предлагает отключить protect_from_forgery на конкретном действии контроллера, которое вызывает это исключение:

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

Вы забыли добавить <%= csrf_meta_tags %> в сторону вашего макета файла.

например:

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

TLDR: Вы, вероятно, видите эту проблему, потому что ваша форма отправляется через XHR.

Сначала несколько вещей:

  1. Rails включает в себя токен CSRF внутри тега head вашей страницы.
  2. Rails оценивает этот токен CSRF каждый раз, когда вы выполняете запрос POST, PATCH или DELETE.
  3. Срок действия этого токена истекает при входе или выходе

Стандартный HTTP-вход в систему из-за болота приведет к полному обновлению страницы, а старый токен CSRF будет сброшен и заменен новым, который Rails создает при входе в систему.

Вход в AJAX не обновит страницу, поэтому на вашей странице все еще присутствует старый, устаревший, устаревший токен CSRF, который теперь недействителен.

Решение состоит в том, чтобы обновить токен CSRF внутри тега HEAD вручную после входа в AJAX.


Некоторые шаги, которые я беззастенчиво позаимствовал из полезной ветки по этому вопросу.

Шаг 1. Добавьте новый CSRF-токен в заголовки ответа, которые отправляются после успешного входа

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

Шаг 2: Используйте jQuery для обновления страницы новыми значениями, когда ajaxComplete события пожары:

$(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 в зависимости от вашей конфигурации Devise. Я подозреваю, однако, что эта проблема в конечном итоге вызвана тем фактом, что старый токен CSRF убивает запрос, а rails создает исключение.

Для Rails 5 это может быть связано с порядком, в котором protect_from_forgery и ваш before_actions срабатывают.

Недавно я столкнулся с подобной ситуацией, хотя protect_from_forgery with: :exception была первая строка в ApplicationController, 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

Если вы используете только API, вы должны попробовать:

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

http://edgeapi.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html

Проблема с кешированием HTML в браузере (2020 г.)

Если вы испробовали все средства на этой странице, но проблема с InvalidAuthenticityTokenисключения, это может быть связано с кешированием HTML браузером. На Github есть проблема с сотнями комментариев и некоторым воспроизводимым кодом. Вкратце, вот что со мной происходило в отношении кеширования HTML:

  1. Пользователь переходит на сайт. Rails устанавливает подписанный файл cookie сеанса на первый запрос GET. Видетьconfig/initializers/session_store.rbдля параметров конфигурации. Этот файл cookie сеанса хранит полезную информацию, включая токен CSRF, который используется для расшифровки и проверки подлинности запроса. Важно: по умолчанию срок действия файла cookie сеанса истекает при закрытии окна браузера.
  2. Пользователь переходит на страницу, содержащую форму. Что касается меня, я получал больше всего исключений на моей странице входа.
  3. Rails встраивает скрытый токен CSRF в эту форму и отправляет этот токен вместе с данными формы. Важно: этот токен встроен в HTML.
  4. ActionController получает токен CSRF из объекта params и проверяет его с помощью токена CSRF из файла cookie, используя verified_request? в Rails 4.2+.

Многие браузеры теперь реализуют кэширование HTML, поэтому при открытии страницы HTML загружается без запроса. К сожалению, когда браузер закрывается, cookie сеанса уничтожается, поэтому, если пользователь закрывает браузер, находясь в форме (например, на странице входа в систему), тогда первый запрос не будет содержать токен CSRF, что приведет к ошибке InvalidAuthenticityError.

Два общих решения

  1. Продлите срок действия файла cookie сеанса за пределы окна браузера.
  2. Определите в браузере, если файл cookie сеанса отсутствует (через прокси-файл cookie), и если он отсутствует, обновите страницу.

1. Продление срока действия файла cookie сеанса

Как отмечено в этом комментарии Github, Django использует такой подход:

Django добавляет токен в свой собственный файл cookie с именем CSRF_COOKIE. Это постоянный файл cookie, срок действия которого истекает через год. Если поступают последующие запросы, срок действия cookie обновляется.

В Rails:

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

Что касается многих вещей, связанных с безопасностью, есть опасения, что это может создать уязвимости, но мне не удалось найти никаких примеров того, как злоумышленник мог бы этим воспользоваться.

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

Проверка этого файла cookie в JS:

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

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

Это заставит браузер обновиться.

Вывод

Я надеюсь, что это поможет некоторым людям; эта ошибка стоила мне нескольких дней работы. Если у вас все еще есть проблемы, подумайте о том, чтобы прочитать:

Просто потратил все утро на отладку этого, поэтому я подумал, что должен поделиться этим здесь, на случай, если кто-то столкнется с аналогичной проблемой при обновлении rails до 5.2 или 6.

У меня было 2 проблемы

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
  • удалить secretts.yml

Вы должны поместить protect_from_forgery прямо перед действием для аутентификации пользователя. Это правильное решение

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate_user!
end
Другие вопросы по тегам