2012-08-07 65 views
34

我正在做一個使用Rails的單頁應用程序。登錄和註銷時使用ajax調用Devise控制器。我得到的問題是,當我1)登錄2)註銷然後再次登錄不起作用。Rails,設計認證,CSRF問題

我認爲它與CSRF令牌有關,當我註銷時它會被重置(儘管它不應該失敗),並且由於它是單頁,舊的CSRF令牌正在xhr請求中發送,從而重置會話。

更具體的是這樣的工作流程:

  1. 登錄
  2. 登出
  3. 登錄(成功201在服務器日誌但是打印WARNING: Can't verify CSRF token authenticity
  4. 隨後的AJAX請求失敗401未經授權
  5. 刷新網站(此時,頁面標題中的CSRF更改爲其他內容)
  6. 我可以登錄,它的工作原理,直到我嘗試退出並重新登錄。

任何線索非常感謝!讓我知道我是否可以添加更多細節。

回答

35

Jimbo做了一個很棒的工作,解釋了你遇到的問題背後的原因。有可以採取的解決問題的兩種方法:

  1. (所推薦的神保)重寫設計:: SessionsController回報新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")); 
    }); 
    
  2. 更加簡單,它是否適合你的應用程序,你可以簡單地覆蓋Devise::SessionsControllerskip_before_filter :verify_authenticity_token覆蓋令牌檢查。

+1

任何人都知道這個問題是否與Devise開發者一起提出? – IanWhalen 2012-12-29 04:10:33

+1

#2對我不起作用,因爲我通過Devise/Ajax通過任何POST請求進行登錄後收到CSRF真實性錯誤。我也不確定如何渲染新的csrf標記,因爲我已經將模板渲染爲我的':create'動作的最後一步。我在這裏提出了一個問題(http://stackoverflow.com/questions/26640326/after-devise-sign-in-via-ajax-any-post-request-results-in-csrf-authenticity-err)並真的很感激,如果你有時刻檢查出來 – sixty4bit 2014-10-29 21:36:19

+1

@ sixty4bit我似乎與你有同樣的問題,但你的問題已被刪除。你有沒有發現它? – 2015-07-21 15:01:36

0

檢查是否已包括這在你的application.js文件

//= require jquery

//= require jquery_ujs

之所以存在是jQuery的軌道寶石在默認情況下會自動將所有Ajax請求的CSRF令牌,需要這兩個

+0

是的,我有這些。我使用開發工具檢查了這些請求,它們都有CSRF。 – vrepsys 2012-08-07 12:17:14

+0

檢查您是否獲得相同的CSRF令牌或不同的令牌 – pdpMathi 2012-08-10 13:21:54

+0

此解決方案適用於我。 感謝和問候, – Icicle 2013-12-06 16:48:40

31

我剛剛遇到了這個問題。這裏有很多事情要做。

TL; DR - 失敗的原因是CSRF令牌與您的服務器會話相關聯(無論您是登錄還是註銷,您都有一個服務器會話)。 CSRF令牌包含在每頁加載的DOM頁面中。在註銷時,您的會話被重置並且沒有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對所有非GET HTML/Javascript請求執行CSRF檢查。由於Devise Login是一個POST,它執行CSRF檢查。如果CSRF檢查失敗,則用戶的當前會話被清除,即將用戶註銷,因爲服務器認爲它是攻擊(這是正確的/期望的行爲)。

所以假設你在登出狀態開始,你做一個新的頁面加載,並且不會再重新加載頁面:

  1. 在渲染頁面:服務器插入CSRF令牌與您的服務器會話關聯到頁面中。您可以通過在瀏覽器中運行以下JavaScript控制檯來查看此令牌$('meta[name="csrf-token"]').attr('content')

  2. 然後,您可以登錄通過一個XMLHttpRequest:你CSRF令牌在這一點上保持不變,所以CSRF令牌在會話仍然匹配插入到頁面中的一個。在後臺,在客戶端,jquery-ujs正在偵聽xhr,並自動爲您設置一個值爲$('meta[name="csrf-token"]').attr('content')的「X-CSRF-Token」頭(請記住這是服務器在步驟1中設置的CSRF令牌) 。服務器比較jquery-ujs在標題中設置的令牌和存儲在會話信息中的令牌,它們匹配以便請求成功。

  3. 然後您通過XMLHttpRequest註銷:這會重置會話,爲您提供沒有CSRF令牌的新會話。

  4. 你然後再次登錄通過一個XMLHttpRequest: jQuery的UJS拉從$('meta[name="csrf-token"]').attr('content')價值CSRF令牌。該值仍然是您的CSRF令牌。它需要這個舊的令牌並使用它來設置'X-CSRF-Token'。服務器將此頭值與它添加到會話中的新CSRF令牌進行比較,這是不同的。這種差異導致protect_form_forgery失敗,這會引發WARNING: Can't verify CSRF token authenticity並重置您的會話,從而將用戶註銷。

  5. 然後你讓一個需要登錄的用戶的另一個的XMLHttpRequest:當前會話沒有登錄的用戶,以便制定回報401

更新:8/14設計註銷不會給你一個新的CSRF令牌,通常在註銷後發生的重定向會給你一個新的csrf令牌。

+0

一旦我找出如何實現TL中的兩個步驟;我會發布代碼。 – plainjimbo 2012-08-13 22:32:57

+0

來自DOM的CSRF值不是一個好的解決方案。 來自Angular Doc:[$ http](http://docs.angularjs.org/api/ng.$http) >由於只有在您的域上運行的JavaScript才能讀取cookie,因此您的服務器可以得到保證XHR來自在您的域上運行的JavaScript。標題不會被設置爲跨域請求。 – chakming 2014-04-10 06:27:20

+1

我有這個相同的問題,但在我的工作流程中,我從不退出。通過XHR登錄後,我無法完成POST操作。因此,我的流程是通過xhr(在控制檯中沒有csrf警告)從新窗口小部件表單登錄 - >嘗試提交新窗口小部件窗體(控制檯中的csrf警告) - >退出並重定向到常規登錄窗體。根據控制檯的說明,'params [:authenticity_token]'在xhr登錄請求和html #create請求期間是相同的。任何幫助? – sixty4bit 2014-10-29 20:15:31

5

挖監獄長源後,我注意到,設置sign_out_all_scopesfalse從清除整個會話停止監獄長,所以CSRF令牌標誌奏之間保留。

上設計問題裝釘相關討論:https://github.com/plataformatec/devise/issues/2200

+0

這個答案解決了我的許多問題。謝謝盧卡斯。 – Matt 2016-02-13 17:52:29

7

這是我的看法:

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 meta標籤更新一次您通過ajax請求返回X-CSRF-TokenX-CSRF-Param標題。

+0

謝謝,我從我的控制器調用'request_forgery_protection_token'和'form_authenticity_token'時不得不使用字符串插值,因爲我收到關於不能在':authenticity_token:String'上調用'split'的錯誤......但是這種技術可行真的很好。謝謝。 – stephenmurdoch 2014-02-06 12:45:00

+2

這是正確的答案,但我不知道重新生成一個新的crsf標記的安全含義。你應該添加如果self.request.format.symbol ==:例如json或任何你不重定向的mime – Gepsens 2014-04-22 11:27:28

+1

我根據你的建議更新了答案。好,趕快,謝謝! – Sija 2014-04-23 22:28:39

8

我的回答來自@Jimbo和@Sija大量借鑑,但是我使用的色器件/ angularjs會議建議在Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST,並闡述了一點在我blog當我最初這樣做。這樣做的應用程序控制器上的方法來設置cookies的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的格式,但使用的代碼從早期做方案,給我:

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 

爲了完整起見,我花了幾分鐘的時間來完成它,我還注意到需要修改config/routes.rb來聲明你已經覆蓋了會話控制器。喜歡的東西:

devise_for :users, :controllers => {sessions: 'sessions'} 

這也是一個大的CSRF清理,我已經在我的應用程序完成的,這可能是有趣的,別人的一部分。 blog post is here,其他更改包括:

從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操作附近的並行請求。

1

我只是在我的佈局文件加入這一點,它在回覆工作

<%= 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 %> 
-1

爲@ sixty4bit的評論;如果你遇到這樣的錯誤:

Unexpected error while processing request: undefined method each for :authenticity_token:Symbol` 

response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s 
+0

這可以回答評論中的問題,但不應該回答這個問題。我建議你創建一個新的問題鏈接到這個問題,並自我回答這個答案。然後,您也可以爲鏈接到新創建的問題添加評論 – Bowdzone 2015-02-27 14:15:40

0

更換

response.headers['X-CSRF-Param'] = request_forgery_protection_token 

在我的情況後,在登錄,我需要重新繪製用戶的菜單中的用戶。這很有效,但是在同一部分(當然,沒有刷新頁面),每次向服務器發送請求時都會收到CSRF真實性錯誤。以上解決方案無法正常工作,因爲我需要呈現js視圖。

我所做的這是什麼,使用設計:

應用程序/控制器/ 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-帕拉姆和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情況:)