Ruby on rails Rails、设计认证、CSRF问题
我正在使用Rails做一个单页应用程序。在登录和注销时,使用ajax调用Device控制器。我遇到的问题是,当我1)登录2)注销时,再次登录不起作用 我认为这与CSRF令牌有关,当我注销时,CSRF令牌会被重置(尽管它不应该是afaik),因为它是单页的,所以旧的CSRF令牌会在xhr请求中发送,从而重置会话 更具体地说,这是工作流程:Ruby on rails Rails、设计认证、CSRF问题,ruby-on-rails,ajax,authentication,devise,csrf,Ruby On Rails,Ajax,Authentication,Devise,Csrf,我正在使用Rails做一个单页应用程序。在登录和注销时,使用ajax调用Device控制器。我遇到的问题是,当我1)登录2)注销时,再次登录不起作用 我认为这与CSRF令牌有关,当我注销时,CSRF令牌会被重置(尽管它不应该是afaik),因为它是单页的,所以旧的CSRF令牌会在xhr请求中发送,从而重置会话 更具体地说,这是工作流程: 登录 注销 登录(成功201。但是打印警告:无法在服务器日志中验证CSRF令牌的真实性) 后续ajax请求未经授权而失败 刷新网站(此时,页面标题中的CSRF将
警告:无法在服务器日志中验证CSRF令牌的真实性
)非常感谢任何线索!如果可以添加更多详细信息,请告诉我。检查您是否已将其包含在application.js文件中 //=需要jquery //=需要jquery\u ujs
原因是jqueryrailsgem默认情况下会自动在所有Ajax请求上设置CSRF令牌,需要这两个标记,我刚才也遇到了这个问题。这里发生了很多事情 TL;DR-失败的原因是CSRF令牌与您的服务器会话相关联(无论您是登录还是注销,您都有一个服务器会话)。每次页面加载时,CSRF令牌都包含在页面的DOM中。注销时,会话将重置,并且没有csrf令牌。通常,注销会重定向到不同的页面/操作,这会为您提供一个新的CSRF令牌,但由于您使用的是ajax,因此需要手动执行此操作
- 您需要重写designe SessionController::destroy方法以返回新的CSRF令牌李>
- 然后在客户端,您需要为注销XMLHttpRequest设置一个成功处理程序。在该处理程序中,您需要从响应中获取这个新的CSRF令牌,并将其设置在dom中:
$('meta[name=“csrf token”]').attr('content',)
protect\u from\u forgery
,您的所有其他控制器都从该文件继承(我认为这很常见)<代码>防止伪造对所有非GET HTML/Javascript请求执行CSRF检查。由于Desive登录是一个POST,因此它执行CSRF检查。如果CSRF检查失败,则会清除用户的当前会话,即注销用户,因为服务器认为这是攻击(这是正确/期望的行为)
因此,假设您在注销状态下启动,您将重新加载页面,并且不再重新加载页面:
$('meta[name=“csrf token”]').attr('content')
中的javascript控制台运行以下命令来查看此令牌$('meta[name=“CSRF Token”]').attr('content')
的“X-CSRF-Token”头(请记住,这是服务器在步骤1中设置的CSRF令牌)。服务器将jquery ujs在头中设置的令牌与存储在会话信息中的令牌进行比较,两者匹配,请求成功$('meta[name=“CSRF token”]').attr('content')
的值中提取CSRF令牌。此值仍然是您的旧CSRF令牌。它接受这个旧令牌并使用它设置“X-CSRF-token”。服务器将此头值与添加到会话中的新CSRF令牌进行比较,这是不同的。此差异导致保护表单伪造失败,引发警告:无法验证CSRF令牌的真实性
并重置会话,从而注销用户
更新:8/14设计注销不会给你一个新的CSRF令牌,通常在注销后发生的重定向会给你一个新的CSRF令牌。Jimbo做了一个很棒的工作,解释了你遇到的问题背后的“原因”。有两种方法可以解决此问题:
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
这还假设您在所有AJAX请求中自动包含CSRF令牌,如下所示:
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
});
designe::sessioncontroller
,并使用skip\u before\u filter:verify\u authenticity\u token
覆盖令牌检查在挖掘了Warden源代码后,我注意到将
sign_out\u all_scopes
设置为false
会阻止Warden清除整个会话,因此在注销之间会保留CSRF令牌
有关设计问题解决者的相关讨论:这是我的观点:
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
每次您通过ajax请求返回
X-CSRF-Token
或X-CSRF-Param
头时,都会更新您的CSRF元标记。我的答案大量借用了@Jimbo和@Sija,但我没有
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);
}
});
after_filter :set_csrf_cookie_for_ng
def set_csrf_cookie_for_ng
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
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
devise_for :users, :controllers => {sessions: 'sessions'}
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
<%= 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 %>
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
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
$('.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 %>');
@new_csrf_token = form_authenticity_token
$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');