Rails CSRF Protection + Angular.js: protect_from_forgery faz com que eu faça logout no POST

Se a opção protect_from_forgery é mencionada em application_controller, então eu posso efetuar login e executar qualquer solicitação GET, mas no primeiro pedido POST, o Rails redefine a session, que faz o logout.

Eu desliguei temporariamente a opção protect_from_forgery , mas gostaria de usá-la com o Angular.js. Existe alguma maneira de fazer isso?

Acho que ler o valor CSRF do DOM não é uma boa solução, é apenas uma solução alternativa.

Aqui está um documento no site oficial do angularJS http://docs.angularjs.org/api/ng.$http :

Como somente o JavaScript que é executado em seu domínio pode ler o cookie, seu servidor pode ter certeza de que o XHR veio do JavaScript em execução no seu domínio.

Para tirar proveito disso (Proteção CSRF), seu servidor precisa definir um token em um cookie de session legível por JavaScript chamado XSRF-TOKEN na primeira solicitação HTTP GET. Em solicitações não GET subsequentes, o servidor pode verificar se o cookie corresponde ao header HTTP X-XSRF-TOKEN

Aqui está a minha solução com base nessas instruções:

Primeiro, defina o 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 

Então, devemos verificar o token em todas as solicitações não-GET.
Como o Rails já construiu com o método similar, podemos simplesmente substituí-lo para append nossa lógica:

 # 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 

Se você estiver usando a proteção padrão Rails CSRF ( <%= csrf_meta_tags %> ), você pode configurar seu módulo Angular assim:

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

Ou, se você não estiver usando o CoffeeScript (o que !?):

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

Se preferir, você pode enviar o header somente em solicitações não-GET com algo como o seguinte:

 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 ] 

Além disso, não se esqueça de verificar a resposta do HungYuHei , que abrange todas as bases do servidor, em vez do cliente.

A gem angular_rails_csrf adiciona automaticamente suporte para o padrão descrito na resposta do HungYuHei para todos os seus controllers:

 # Gemfile gem 'angular_rails_csrf' 

A resposta que mescla todas as respostas anteriores e confirma que você está usando a gema de autenticação do Devise .

Primeiro de tudo, adicione a gema:

 gem 'angular_rails_csrf' 

Em seguida, adicione o bloco rescue_from no 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 

E, finalmente, adicione o módulo interceptor ao aplicativo 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') 

Eu vi as outras respostas e pensei que elas eram ótimas e bem pensadas. Eu tenho meu aplicativo de rails trabalhando com o que eu pensava ser uma solução mais simples, então eu pensei em compartilhar. Meu aplicativo de rails veio com este padrão nele,

 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 

Eu li os comentários e parecia que é o que eu quero usar angular e evitar o erro csrf. Eu mudei para isso,

 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 

E agora funciona! Eu não vejo nenhuma razão para isso não funcionar, mas eu adoraria ouvir algumas idéias de outros pôsteres.

Eu usei o conteúdo da resposta do HungYuHei na minha aplicação. Descobri que eu estava lidando com alguns problemas adicionais, no entanto, alguns por causa do meu uso do Devise para autenticação, e alguns por causa do padrão que recebi com o meu aplicativo:

 protect_from_forgery with: :exception 

Observo a questão relacionada ao estouro de pilha e as respostas lá , e escrevi um post muito mais detalhado que resume as várias considerações. As partes dessa solução que são relevantes aqui são, no controlador de aplicativo:

  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 

Eu achei um truque muito rápido para isso. Tudo que eu tive que fazer é o seguinte:

uma. A meu ver, inicializo uma variável $scope que contém o token, digamos antes do formulário, ou melhor ainda na boot do controlador:

 

b. No meu controlador AngularJS, antes de salvar minha nova input, adiciono o token ao hash:

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

Nada mais precisa ser feito.

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

Está trabalhando no lado angular!