【问题标题】:Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POSTRails CSRF 保护 + Angular.js:protect_from_forgery 让我在 POST 上注销
【发布时间】:2013-01-21 22:57:27
【问题描述】:

如果在 application_controller 中提到了protect_from_forgery 选项,那么我可以登录并执行任何 GET 请求,但在第一个 POST 请求时,Rails 会重置会话,这会将我注销。

我暂时关闭了 protect_from_forgery 选项,但想将它与 Angular.js 一起使用。有什么办法吗?

【问题讨论】:

标签: ruby-on-rails angularjs csrf protect-from-forgery


【解决方案1】:

如果你使用默认的 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's answer,它涵盖了服务器而非客户端的所有基础。

【讨论】:

  • 让我解释一下。基本文档是纯 HTML,而不是 .erb,因此我不能使用 <%= csrf_meta_tags %>。我认为应该足以仅提及protect_from_forgery。该怎么办?基础文档必须是纯 HTML(我在这里不是选择的人)。
  • 当您使用protect_from_forgery 时,您的意思是“当我的JavaScript 代码发出Ajax 请求时,我承诺在与当前CSRF 令牌对应的标头中发送X-CSRF-Token。”为了获得这个令牌,Rails 使用<%= csrf_meta_token %> 将其注入到 DOM 中,并在每次发出 Ajax 请求时使用 jQuery 获取元标记的内容(默认的 Rails 3 UJS 驱动程序会为您执行此操作)。如果您不使用 ERB,则无法将当前令牌从 Rails 获取到页面和/或 JavaScript 中——因此您不能以这种方式使用 protect_from_forgery
  • 感谢您的解释。我认为在经典的服务器端应用程序中,客户端每次服务器生成响应时都会收到csrf_meta_tags,并且每次这些标签都与以前的标签不同。因此,这些标签对于每个请求都是唯一的。问题是:应用程序如何为 AJAX 请求(没有角度)接收这些标签?我在 jQuery POST 请求中使用了protect_from_forgery,从不为获取这个 CSRF 令牌而烦恼,它确实有效。怎么样?
  • Rails UJS 驱动程序使用jQuery.ajaxPrefilter 如下所示:github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/… 您可以仔细阅读此文件并查看 Rails 跳过的所有环节,使其几乎可以正常工作,而不必担心。
  • @BrandonTilley 只在putpost 而不是common 上这样做不是很有意义吗?来自rails security guideThe solution to this is including a security token in non-GET requests
【解决方案2】:

我认为从 DOM 读取 CSRF-value 不是一个好的解决方案,它只是一种解决方法。

这里是angularJS官网http://docs.angularjs.org/api/ng.$http的文档形式:

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

要利用这一点(CSRF 保护),您的服务器需要在 JavaScript 可读会话中设置一个令牌 在第一个 HTTP GET 请求中调用 XSRF-TOKEN 的 cookie。在随后 非 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

【讨论】:

  • 我喜欢这种技术,因为您不必修改任何客户端代码。
  • 这个解决方案如何保持 CSRF 保护的有用性?通过设置 cookie,被标记用户的浏览器将在所有后续请求(包括跨站点请求)上发送该 cookie。我可以设置发送恶意请求的恶意第三方站点,并且用户的浏览器会将“XSRF-TOKEN”发送到服务器。看来这个解决方案等于完全关闭了 CSRF 保护。
  • 来自 Angular 文档:“由于只有在您的域上运行的 JavaScript 才能读取 cookie,因此您的服务器可以确信 XHR 来自在您的域上运行的 JavaScript。” @StevenXu - 第三方网站如何读取 cookie?
  • @JimmyBaker:是的,你是对的。我已经查看了文档。这种方法在概念上是合理的。我将 cookie 的设置与验证混淆了,没有意识到 Angular 框架正在根据 cookie 的值设置自定义标头!
  • form_authenticity_token 在 Rails 4.2 中的每次调用时都会生成新值,所以这似乎不再起作用了。
【解决方案3】:

我看到了其他答案,并认为它们很棒而且经过深思熟虑。我让我的 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

我阅读了 cmets,这似乎是我想要使用 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

现在它可以工作了!我看不出有什么理由不应该这样做,但我很想听听其他海报的一些见解。

【讨论】:

  • 如果您尝试使用 rails 'sessions',这将导致问题,因为如果伪造测试失败,它将被设置为 nil,因为您没有发送 csrf-来自客户端的令牌。
  • 但是如果你不使用 Rails 会话,一切都很好;谢谢!我一直在努力寻找最干净的解决方案。
【解决方案4】:

angular_rails_csrf gem 会自动为您的所有控制器添加对HungYuHei's answer 中描述的模式的支持:

# Gemfile
gem 'angular_rails_csrf'

【讨论】:

  • 知道如何配置应用程序控制器和其他 csrf/伪造相关设置,以正确使用 angular_rails_csrf?
  • 在发表此评论时,angular_rails_csrf gem 不适用于 Rails 5。但是,使用 CSRF 元标记中的值配置 Angular 请求标头有效!
  • 有一个新版本的 gem,它支持 Rails 5。
【解决方案5】:

我在我的应用程序中使用了 HungYuHei 回答的内容。然而,我发现我正在处理一些其他问题,其中一些是因为我使用 Devise 进行身份验证,还有一些是因为我的应用程序默认设置:

protect_from_forgery with: :exception

我注意到相关的stack overflow question and the answers there,我写了一个更详细的blog post,总结了各种注意事项。该解决方案的相关部分在应用程序控制器中:

  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

【讨论】:

    【解决方案6】:

    合并所有先前答案的答案,它依赖于您正在使用Devise 身份验证gem。

    首先,添加gem:

    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')
    

    【讨论】:

    • 你为什么要注入$injector而不是直接注入$http
    • 这行得通,但我认为我添加的只是检查请求是否已经重复。当它被重复时,我们不会再次发送,因为它会永远循环。
    【解决方案7】:

    我发现了一个非常快速的破解方法。我所要做的就是:

    一个。在我看来,我初始化了一个包含令牌的$scope 变量,比如说在表单之前,或者在控制器初始化时更好:

    <div ng-controller="MyCtrl" ng-init="authenticity_token = '<%= form_authenticity_token %>'">
    

    b.在我的 AngularJS 控制器中,在保存我的新条目之前,我将令牌添加到哈希中:

    $scope.addEntry = ->
        $scope.newEntry.authenticity_token = $scope.authenticity_token 
        entry = Entry.save($scope.newEntry)
        $scope.entries.push(entry)
        $scope.newEntry = {}
    

    没有什么需要做的了。

    【讨论】:

      【解决方案8】:
       angular
        .module('corsInterceptor', ['ngCookies'])
        .factory(
          'corsInterceptor',
          function ($cookies) {
            return {
              request: function(config) {
                config.headers["X-XSRF-TOKEN"] = $cookies.get('XSRF-TOKEN');
                return config;
              }
            };
          }
        );
      

      它正在 angularjs 方面工作!

      【讨论】:

        猜你喜欢
        • 2019-07-14
        • 2014-08-12
        • 2010-12-22
        • 2014-04-13
        • 2014-02-19
        • 1970-01-01
        • 2011-06-27
        • 2019-05-16
        • 2013-03-14
        相关资源
        最近更新 更多