Rails, проверка подлинности, проблема CSRF
Я использую одностраничное приложение, использующее Rails. При входе в систему и выходе из нее контроллеры Devise вызывается с помощью ajax. Проблема, которую я получаю, заключается в том, что когда я 1) подписываюсь в 2) выходим, тогда вход в систему снова не работает.
Я думаю, что это связано с токеном CSRF, который получает reset, когда я выхожу (хотя это не должно быть afaik), и поскольку это одна страница, старый токен CSRF отправляется в xhr-запросе, таким образом, сброс сеанса.
Чтобы быть более конкретным, это рабочий процесс:
- Войти
- Выйти
- Войдите (успешно 201. Однако печатает
WARNING: Can't verify CSRF token authenticity
в журналах сервера)
- Последующий запрос ajax сбой 401 неавторизованный
- Обновите сайт (на данный момент CSRF в заголовке страницы изменяется на что-то еще)
- Я могу войти, он работает, пока я не попытаюсь выйти и снова.
Любые подсказки очень ценятся! Дайте мне знать, если я могу добавить более подробную информацию.
Ответы
Ответ 1
Jimbo сделал потрясающую работу, объясняющую "почему" за проблемой, с которой вы столкнулись. Есть два подхода, которые вы можете предпринять для решения проблемы:
-
(Как рекомендовано Jimbo) Override Devise:: SessionController для возврата нового токена csrf:
class SessionsController < Devise::SessionsController
def destroy # Assumes only JSON requests
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
render :json => {
'csrfParam' => request_forgery_protection_token,
'csrfToken' => form_authenticity_token
}
end
end
И создайте обработчик успеха для вашего запроса sign_out на стороне клиента (вероятно, вам нужны некоторые настройки на основе вашей установки, например GET vs DELETE):
signOut: function() {
var params = {
dataType: "json",
type: "GET",
url: this.urlRoot + "/sign_out.json"
};
var self = this;
return $.ajax(params).done(function(data) {
self.set("csrf-token", data.csrfToken);
self.unset("user");
});
}
Это также предполагает, что вы включаете токен CSRF автоматически со всеми запросами AJAX с чем-то вроде этого:
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
});
-
Гораздо проще, если это подходит для вашего приложения, вы можете просто переопределить Devise::SessionsController
и переопределить проверку маркера с помощью skip_before_filter :verify_authenticity_token
.
Ответ 2
Я тоже столкнулся с этой проблемой. Здесь многое происходит.
TL; DR. Причина отказа заключается в том, что токен CSRF связан с вашим сеансом сервера (у вас есть сеанс сервера, независимо от того, вошли ли вы в систему или вышли из системы). Маркер CSRF включен на страницу DOM на каждой странице загрузки. При выходе из системы ваша сессия reset и не имеет токена csrf. Как правило, выход из системы перенаправляется на другую страницу/действие, что дает вам новый токен CSRF, но поскольку вы используете ajax, вам нужно сделать это вручную.
- Вам необходимо переопределить метод Devise SessionController:: destroy, чтобы вернуть новый токен CSRF.
- Затем на стороне клиента вам нужно установить обработчик успеха для вашего выхода XMLHttpRequest. В этом обработчике вам нужно взять этот новый токен CSRF из ответа и установить его в своем dom:
$('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)
Подробное пояснение Скорее всего, вы получили protect_from_forgery
в вашем файле ApplicationController.rb, из которого наследуются все ваши другие контроллеры (это довольно часто, как мне кажется). protect_from_forgery
выполняет проверки CSRF для всех запросов, не связанных с GET HTML/Javascript. Поскольку Devise Login является POST, он выполняет проверку CSRF. Если CSRF Check завершается с ошибкой, то текущий сеанс пользователя очищается, то есть регистрирует пользователя, потому что сервер принимает на себя атаку (что является правильным/желаемым поведением).
Итак, предполагая, что вы начинаете в состоянии выхода из системы, вы загружаете новую страницу и никогда не перезагружаете страницу:
-
При рендеринге страницы: сервер вставляет в страницу токен CSRF, связанный с вашим сеансом сервера. Вы можете просмотреть этот токен, выполнив следующее из консоли javascript в своем браузере $('meta[name="csrf-token"]').attr('content')
.
-
Затем вы регистрируетесь через XMLHttpRequest: Ваш токен CSRF остается неизменным в этот момент, поэтому токен CSRF на вашем сеансе по-прежнему совпадает с тем, который был вставлен на страницу. За кулисами, на стороне клиента, jquery-ujs прослушивает xhr и устанавливает заголовок "X-CSRF-Token" со значением $('meta[name="csrf-token"]').attr('content')
для вас автоматически (помните, что это был токен CSRF, установленный на шаге 1, с помощью разъединить). Сервер сравнивает набор токенов в заголовке с помощью jquery-ujs и тот, который хранится в вашей информации о сеансе, и они совпадают, чтобы запрос удался.
-
Затем вы выходите через XMLHttpRequest:. Сбрасывает сеанс, дает вам новый сеанс без токена CSRF.
-
Затем вы снова регистрируетесь через XMLHttpRequest: jquery-ujs вытаскивает токен CSRF со значения $('meta[name="csrf-token"]').attr('content')
. Это значение по-прежнему является вашим токеном OLD CSRF. Он принимает этот старый токен и использует его для установки "X-CSRF-токена". Сервер сравнивает это значение заголовка с новым токеном CSRF, который он добавляет к вашему сеансу, который отличается. Это различие приводит к сбою protect_form_forgery
, который выдает WARNING: Can't verify CSRF token authenticity
и сбрасывает ваш сеанс, который регистрирует пользователя.
-
Затем вы создаете другое XMLHttpRequest, для которого требуется зарегистрированный пользователь: В текущем сеансе нет зарегистрированного пользователя, поэтому devolution возвращает 401.
Обновление: 8/14 Выйти из системы не дает вам новый токен CSRF, перенаправление, которое обычно происходит после выхода из системы, дает вам новый токен csrf.
Ответ 3
Мой ответ сильно зависит от обоих @Jimbo и @Sija, однако я использую соглашение о разработке/углов, предложенное в Rails CSRF Protection + Angular.js: protect_from_forgery заставляет меня чтобы выйти из POST, и немного разработал мой блог, когда я изначально это сделал. У этого есть метод на контроллере приложения для установки файлов cookie для csrf:
after_filter :set_csrf_cookie_for_ng
def set_csrf_cookie_for_ng
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
Итак, я использую формат @Sija, но используя код из этого более раннего SO-решения, даю мне:
class SessionsController < Devise::SessionsController
after_filter :set_csrf_headers, only: [:create, :destroy]
protected
def set_csrf_headers
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
end
Для полноты, так как мне понадобилось пару минут, чтобы разобраться в этом, я также хочу отметить, что вам нужно изменить конфигурацию /routes.rb, чтобы объявить, что вы переопределили контроллер сеансов. Что-то вроде:
devise_for :users, :controllers => {sessions: 'sessions'}
Это также было частью большой очистки CSRF, которую я сделал в своем приложении, что может быть интересно другим. сообщение в блоге здесь, другие изменения включают в себя:
Спасение от ActionController:: InvalidAuthenticityToken, что означает, что если что-то не синхронизируется, приложение будет исправлять себя, а не пользователю, нуждающемуся в очистке файлов cookie. Поскольку вещи стоят в рельсах, я думаю, что ваш контроллер приложения будет дефолт:
protect_from_forgery with: :exception
В этой ситуации вам тогда понадобится:
rescue_from ActionController::InvalidAuthenticityToken do |exception|
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
render :error => 'invalid token', {:status => :unprocessable_entity}
end
У меня также было некоторое горе с условиями гонки и некоторыми взаимодействиями с тайм-аут-модулем в Devise, о чем я прокомментировал дальше в сообщении в блоге. Короче говоря, вам следует рассмотреть возможность использования active_record_store, а не cookie_store, и быть осторожным о выдаче параллельных запросов рядом с sign_in и sign_out действиями.
Ответ 4
Это мое взятие:
class SessionsController < Devise::SessionsController
after_filter :set_csrf_headers, only: [:create, :destroy]
respond_to :json
protected
def set_csrf_headers
if request.xhr?
response.headers['X-CSRF-Param'] = request_forgery_protection_token
response.headers['X-CSRF-Token'] = form_authenticity_token
end
end
end
И на стороне клиента:
$(document).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);
}
});
который будет обновлять метатеги CSRF каждый раз, когда вы возвращаете заголовок X-CSRF-Token
или X-CSRF-Param
через запрос ajax.
Ответ 5
После того, как рыть на источник Уорден, я заметил, что установка sign_out_all_scopes
на false
остановки Warden сбрасывались всю сессию, поэтому маркер CSRF сохраняется между выходами знака.
Связанное обсуждение проблемы разработчика Devise: https://github.com/plataformatec/devise/issues/2200
Ответ 6
Я просто добавил это в мой файл макета, и он работал
<%= csrf_meta_tag %>
<%= javascript_tag do %>
jQuery(document).ajaxSend(function(e, xhr, options) {
var token = jQuery("meta[name='csrf-token']").attr("content");
xhr.setRequestHeader("X-CSRF-Token", token);
});
<% end %>
Ответ 7
Проверьте, включили ли вы это в файл application.js
//= требуется jquery
//= требуется jquery_ujs
Причиной этого является jquery-rails gem, который по умолчанию автоматически устанавливает токен CSRF для всех запросов Ajax, для этих двух
Ответ 8
В моем случае, после входа в систему пользователя, мне нужно было перерисовать меню пользователя. Это сработало, но я получил ошибки подлинности CSRF при каждом запросе на сервер, в том же разделе (без обновления страницы, конечно). Выше решения не работали, так как мне нужно было отобразить js-представление.
Я сделал это, используя Devise:
приложение/контроллеры/sessions_controller.rb
class SessionsController < Devise::SessionsController
respond_to :json
# GET /resource/sign_in
def new
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
yield resource if block_given?
if request.format.json?
markup = render_to_string :template => "devise/sessions/popup_login", :layout => false
render :json => { :data => markup }.to_json
else
respond_with(resource, serialize_options(resource))
end
end
# POST /resource/sign_in
def create
if request.format.json?
self.resource = warden.authenticate(auth_options)
if resource.nil?
return render json: {status: 'error', message: 'invalid username or password'}
end
sign_in(resource_name, resource)
render json: {status: 'success', message: '¡User authenticated!'}
else
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
end
end
После этого я сделал запрос на действие контроллера #, которое перерисовывает меню. И в javascript я модифицировал X-CSRF-Param и X-CSRF-токен:
Приложение/просмотров/утилиты/redraw_user_menu.js.erb
$('.js-user-menu').html('');
$('.js-user-menu').append('<%= escape_javascript(render partial: 'shared/user_name_and_icon') %>');
$('meta[name="csrf-param"]').attr('content', '<%= request_forgery_protection_token.to_s %>');
$('meta[name="csrf-token"]').attr('content', '<%= form_authenticity_token %>');
Я надеюсь, что это полезно для кого-то в той же ситуации js:)
Ответ 9
Моя ситуация была еще проще. В моем случае все, что я хотел сделать, это то, что: если человек сидит на экране с формой и время его сеанса истекло (разработайте тайм-аут сеанса), обычно, если он нажимает кнопку Отправить в этот момент, Devise возвращает его обратно на экран входа в систему. Ну, я не хотел этого, потому что они теряют все свои данные формы. Я использую JavaScript для перехвата отправки формы, Ajax вызывает контроллер, который определяет, больше ли пользователь не вошел в систему, и если это так, я создаю форму, где они повторно вводят свой пароль, и я заново аутентифицирую их (bypass_sign_in в контроллере) используя вызов Ajax. Тогда исходная форма отправки разрешается продолжить.
Работал отлично, пока я не добавил protect_from_forgery.
Итак, благодаря приведенным выше ответам все, что мне действительно нужно, было в моем контроллере, где я снова регистрирую пользователя (bypass_sign_in). Я просто установил переменную экземпляра для нового токена CSRF:
@new_csrf_token = form_authenticity_token
и затем в .js.erb, который был обработан (так как снова это был вызов XHR):
$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');
Вуаля. Моя страница формы, которая не была обновлена и поэтому была привязана к старому токену, теперь содержит новый токен из нового сеанса, который я получил от входа в систему моего пользователя.
Ответ 10
в ответ на комментарий @sixty4bit; если вы столкнулись с этой ошибкой:
Unexpected error while processing request: undefined method each for :authenticity_token:Symbol`
заменить
response.headers['X-CSRF-Param'] = request_forgery_protection_token
с
response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s