Rails CSRF保护+Angular.js:protect_from_forgery在POST请求时让我注销登录

131
如果在application_controller中提到了protect_from_forgery选项,我可以登录并执行任何GET请求,但在第一个POST请求上,Rails会重置会话,将我登出。
我暂时关闭了protect_from_forgery选项,但想要与Angular.js一起使用它。有什么办法可以做到这一点吗?

看看这个能否有所帮助,它是关于设置HTTP头的:https://dev59.com/G2Yq5IYBdhLWcg3wzDvk - Mark Rajcok
8个回答

278

我认为从DOM中读取CSRF值并不是一个好的解决方案,它只是一种权宜之计。

以下是来自AngularJS官网的文档http://docs.angularjs.org/api/ng.$http

由于只有在您的域上运行的JavaScript才能读取cookie,因此您的服务器可以确定XHR是否来自JavaScript在您的域上运行。

要利用此(CSRF保护),您的服务器需要在第一个HTTP GET请求中设置一个令牌,该令牌存储在JavaScript可读的会话cookie XSRF-TOKEN中。在随后的非GET请求中,服务器可以验证cookie是否与X-XSRF-TOKEN HTTP标头相匹配

这是基于这些说明的我的解决方案:

首先,设置cookie:

# app/controllers/application_controller.rb

# Turn on request forgery protection
protect_from_forgery

after_action :set_csrf_cookie

def set_csrf_cookie
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end

然后,我们应该在每个非GET请求上验证令牌。
由于Rails已经使用了类似的方法,我们可以简单地覆盖它以附加我们的逻辑:

# app/controllers/application_controller.rb

protected
  
  # In Rails 4.2 and above
  def verified_request?
    super || valid_authenticity_token?(session, request.headers['X-XSRF-TOKEN'])
  end

  # In Rails 4.1 and below
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

18
我喜欢这种技术,因为你不需要修改任何客户端代码。 - Michelle Tilley
11
这个解决方案如何保留CSRF保护的有效性?通过设置Cookie,标记用户的浏览器将在所有后续请求中发送该Cookie,包括跨站请求。我可以设置一个恶意的第三方网站,向服务器发送恶意请求,然后用户的浏览器就会发送“XSRF-TOKEN”。这似乎等同于完全关闭CSRF保护。 - Steven
10
根据 Angular 文档:"由于只有在您的域上运行的 JavaScript 可以读取 cookie,因此您的服务器可以确保 XHR 来自在您的域上运行的 JavaScript。" @StevenXu - 第三方网站如何读取 cookie? - Jimmy Baker
9
@JimmyBaker:是的,你说得对。我已经审查了文档。这种方法在概念上是可行的。我混淆了cookie的设置和验证,没有意识到Angular框架基于cookie的值设置了自定义头! - Steven
5
在Rails 4.2中,form_authenticity_token在每次调用时都会生成新的值,因此这似乎已经不起作用了。 - Dave
显示剩余15条评论

78

如果你正在使用默认的Rails CSRF保护(<%= csrf_meta_tags %>),你可以像这样配置你的Angular模块:

myAngularApp.config ["$httpProvider", ($httpProvider) ->
  $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
]

或者,如果你没有使用CoffeeScript(什么!?):

myAngularApp.config([
  "$httpProvider", function($httpProvider) {
    $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');
  }
]);

如果您愿意,可以像以下代码一样仅在非GET请求中发送标头:

myAngularApp.config ["$httpProvider", ($httpProvider) ->
  csrfToken = $('meta[name=csrf-token]').attr('content')
  $httpProvider.defaults.headers.post['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.put['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.patch['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.delete['X-CSRF-Token'] = csrfToken
]

此外,请务必查看HungYuHei的答案,该答案涵盖了服务器方面而非客户端的所有基础知识。


让我解释一下。基础文档是一个普通的HTML文件,而不是.erb文件,因此我无法使用<%= csrf_meta_tags %>。我认为只需要提到protect_from_forgery就足够了。怎么办?基础文档必须是一个普通的HTML文件(我在这里不是选择者)。 - Paul
3
当你使用protect_from_forgery时,实际上是在告诉Rails:"当我的JavaScript代码发起Ajax请求时,我保证在请求头中发送一个对应当前CSRF令牌的X-CSRF-Token"。为了获取这个令牌,Rails会用<%= csrf_meta_token %>将它注入到DOM中,并在每次发起Ajax请求时(默认情况下,Rails 3 UJS驱动程序会代劳),使用jQuery获取meta标签的内容。如果你没有使用ERB,则无法从Rails中获取当前令牌并将其传递给页面和/或JavaScript代码--因此,你不能使用protect_from_forgery以这种方式进行防护。 - Michelle Tilley
1
Rails UJS驱动程序使用jQuery.ajaxPrefilter,如此所示:https://github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/javascripts/jquery_ujs.js#L294 您可以查看此文件并查看Rails跳过的所有障碍,使其基本上无需担心即可正常工作。 - Michelle Tilley
如何在不使用jQuery $ ('meta [name = csrf-token]') .attr('content')的情况下访问csrfToken?由于携带XSRF-TOKEN$cookiesconfig块中不可用,并且在run中更新headers没有任何好处。 - red-devil
我在最后一个遇到了问题。$httpProvider.defaults.headers.delete['X-CSRF-Token'] = csrfToken 引发了以下错误:$injector:modulerr] 因为 undefined 不是一个对象 (评估 '$httpProvider.defaults.headers["delete"]['X-CSRF-Token'] = csrfToken'),无法实例化 AngulaRails 模块。 - ctilley79
显示剩余4条评论

29

你有没有想过如何配置你的应用控制器和其他与CSRF/伪造相关的设置,以正确使用angular_rails_csrf? - Ben Wheeler
在本评论发布时,angular_rails_csrf gem 与 Rails 5 不兼容。然而,使用 CSRF meta 标签的值配置 Angular 请求头可以解决问题! - bideowego
有一个新版本的 gem,支持 Rails 5。 - jsanders

4

这个答案汇总了之前所有的答案,并假设您正在使用Devise认证宝石。

首先,添加宝石:

gem 'angular_rails_csrf'

接下来,将rescue_from块添加到application_controller.rb文件中:
protect_from_forgery with: :exception

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  render text: 'Invalid authenticity token', status: :unprocessable_entity
end

最后,将拦截器模块添加到您的Angular应用程序中。
# coffee script
app.factory 'csrfInterceptor', ['$q', '$injector', ($q, $injector) ->
  responseError: (rejection) ->
    if rejection.status == 422 && rejection.data == 'Invalid authenticity token'
        deferred = $q.defer()

        successCallback = (resp) ->
          deferred.resolve(resp)
        errorCallback = (resp) ->
          deferred.reject(resp)

        $http = $http || $injector.get('$http')
        $http(rejection.config).then(successCallback, errorCallback)
        return deferred.promise

    $q.reject(rejection)
]

app.config ($httpProvider) ->
  $httpProvider.interceptors.unshift('csrfInterceptor')

1
为什么你要注入 $injector 而不是直接注入 $http - whitehat101
这个可以工作,但我添加的唯一一点是检查请求是否已经重复。如果它已经重复了,我们就不会再发送它,因为它会无限循环。 - duleorlovic

1
我发现了一个非常快速的解决方法。我所要做的就是以下操作:
a. 在我的视图中,我初始化了一个包含令牌的$scope变量,可以在表单之前进行初始化,或者更好的方法是在控制器初始化时进行:
<div ng-controller="MyCtrl" ng-init="authenticity_token = '<%= form_authenticity_token %>'">

在我的AngularJS控制器中,在保存新条目之前,我将令牌添加到哈希表中:
$scope.addEntry = ->
    $scope.newEntry.authenticity_token = $scope.authenticity_token 
    entry = Entry.save($scope.newEntry)
    $scope.entries.push(entry)
    $scope.newEntry = {}

什么都不需要做。


1
我看到其他的答案,认为它们很好而且思路清晰。不过我认为我用了更简单的方法让我的rails应用程序运行起来,所以我想分享一下。我的rails应用程序默认带有以下代码:
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
end

我阅读了评论,似乎使用angular并避免csrf错误是我想要的。我将其更改为以下内容:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session
end

现在它可以运行了!我没有看到任何原因不能工作,但我很想听听其他帖子的见解。

7
如果您尝试使用Rails的“sessions”功能,这将会导致问题,因为如果它未通过伪造测试(即您没有从客户端发送csrf-token),则会将其设置为nil,这种情况总是会发生。请注意,这里只是翻译原文,而不会提供任何解释或其他内容。 - hajpoj
但是如果您没有使用Rails会话,那么一切都很好;谢谢!我一直在努力寻找最干净的解决方案。 - Morgan

1

我在我的应用程序中使用了HungYuHei的答案内容。然而,我发现我还有一些额外的问题需要解决,其中一些是因为我使用了Devise进行身份验证,一些是因为我得到了默认设置:

protect_from_forgery with: :exception

我注意到相关的Stack Overflow问题和答案,并写了一篇更加详细的博客文章,总结了各种考虑因素。这个解决方案中与此相关的部分在应用控制器中:
  protect_from_forgery with: :exception

  after_filter :set_csrf_cookie_for_ng

  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

  rescue_from ActionController::InvalidAuthenticityToken do |exception|
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
    render :error => 'Invalid authenticity token', {:status => :unprocessable_entity} 
  end

protected
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

0
 angular
  .module('corsInterceptor', ['ngCookies'])
  .factory(
    'corsInterceptor',
    function ($cookies) {
      return {
        request: function(config) {
          config.headers["X-XSRF-TOKEN"] = $cookies.get('XSRF-TOKEN');
          return config;
        }
      };
    }
  );

它正在AngularJS方面工作!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接