programing

레일, 인증 설계, CSRF 문제

kakaobank 2023. 3. 28. 22:05
반응형

레일, 인증 설계, CSRF 문제

나는 레일즈를 이용하여 싱글 페이지 애플리케이션을 하고 있다.로그인 및 로그아웃 시 Ajax를 사용하여 Gand 컨트롤러를 호출합니다.문제가 발생하고 있는 것은 1) 사인아웃을 해도 다시 사인할 수 없다는 것입니다.

사인아웃 시 리셋되는 CSRF 토큰과 관련된 것이라고 생각합니다(단, 1페이지이므로 이전 CSRF 토큰이 xhr 요청으로 전송되어 세션이 리셋됩니다).

좀 더 구체적으로 말하면 다음과 같은 워크플로우입니다.

  1. 사인인
  2. 로그아웃
  3. 201번입니다., 인쇄하다, 인쇄하다WARNING: Can't verify CSRF token authenticity★★★★★★★★★★★★★★★★★★」
  4. 후속 Ajax 요청이 승인되지 않은 401에 실패했습니다.
  5. 웹 사이트를 새로 고칩니다(이 시점에서 페이지 헤더의 CSRF가 다른 것으로 변경됩니다).
  6. 로그아웃을 시도하고 다시 로그인 할 때까지 서명할 수 있습니다.

어떤 힌트라도 매우 감사합니다!더 자세한 내용을 추가할 수 있으면 알려주세요.

Jimbo는 당신이 직면한 문제의 배후에 있는 "이유"에 대해 훌륭하게 설명했습니다.이 문제를 해결하려면 다음 두 가지 방법을 사용할 수 있습니다.

  1. (김보 추천)Override Gand:: SessionsController를 사용하여 새로운 csrf-token을 반환합니다.

    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");
      });
    }
    

    또한 다음과 같은 모든 AJAX 요구에 CSRF 토큰을 자동으로 포함한다고 가정합니다.

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  2. '보다 낫다', '보다 낫다', '보다 낫다', '보다 낫다', '보다 낫다'를 수 .Devise::SessionsController.skip_before_filter :verify_authenticity_token.

저도 방금 이 문제에 부딪혔어요.여기 많은 일들이 있어.

TL;DR - CSRF 토큰이 서버 세션과 관련되어 있기 때문입니다(로그인 또는 로그아웃 여부에 관계없이 서버 세션이 있습니다).CSRF 토큰은 페이지가 로드될 때마다 DOM에 포함됩니다.로그아웃 시 세션은 리셋되고 CSRF 토큰은 없습니다.보통 로그아웃은 다른 페이지/액션으로 리다이렉트 되어 새로운 CSRF 토큰이 제공되지만, Ajax를 사용하고 있기 때문에 수동으로 이 작업을 수행해야 합니다.

  • 새로운 CSRF 토큰을 반환하려면 Gand Session Controller::destroy 메서드를 덮어써야 합니다.
  • 다음으로 클라이언트 측에서 로그아웃 XMLHttpRequest 성공 핸들러를 설정해야 합니다.에서는, 이 CSRF 「CSRF」의 dom 에 .$('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

자세한 설명은 이쪽에서 확인하실 수 있습니다.protect_from_forgery다른 모든 컨트롤러가 상속되는 ApplicationController.rb 파일로 설정합니다(이것은 매우 일반적인 것이라고 생각합니다). protect_from_forgery는 모든 요구에 합니다.GET HTMLJavascript는 CSRF 체크입니다.Gander Login g POST 、 CSRF 、 CSRF g g g g g 。CSRF 체크에 실패하면 사용자의 현재 세션이 클리어 됩니다.즉, 서버는 CSRF 체크가 공격이라고 간주하기 때문에(이것은 올바른 동작 또는 바람직한 동작입니다).

따라서 로그아웃 상태에서 시작할 경우 새 페이지를 로드하고 페이지를 다시 로드하지 않습니다.

  1. 페이지 렌더링 시:서버는 서버 세션과 관련된 CSRF 토큰을 페이지에 삽입합니다.브라우저의 Javascript 콘솔에서 다음을 실행하여 이 토큰을 볼 수 있습니다.$('meta[name="csrf-token"]').attr('content').

  2. 그런 다음 XMLHttpRequest를 통해 로그인합니다.CSRF 토큰은 이 시점에서도 변경되지 않으므로 세션의 CSRF 토큰은 페이지에 삽입된 토큰과 일치합니다.백그라운드에서 클라이언트 측에서는 jquery-ujs가 xhr을 수신하고 다음 값을 사용하여 'X-CSRF-Token' 헤더를 설정합니다.$('meta[name="csrf-token"]').attr('content')(CSRF).서버는 헤더에 설정된 토큰을 jquery-ujs와 세션 정보에 저장되어 있는 토큰을 비교하고 두 토큰이 일치하여 요청이 성공합니다.

  3. 그런 다음 XMLHttpRequest를 통해 로그아웃합니다.그러면 세션이 리셋되고 CSRF 토큰이 없는 새 세션이 제공됩니다.

  4. 그런 다음 XMLHttpRequest를 통해 다시 로그인합니다.jquery-ujs는 다음 값에서 CSRF 토큰을 꺼냅니다.$('meta[name="csrf-token"]').attr('content')이 값은 여전히 OLD CSRF 토큰입니다.이 오래된 토큰을 사용하여 'X-CSRF-Token'을 설정합니다.서버는 이 헤더 값을 세션에 추가하는 새로운 CSRF 토큰과 비교합니다.이것은 다릅니다.이 차이에 의해protect_form_forgeryWARNING: Can't verify CSRF token authenticity세션이 리셋되어 사용자가 로그아웃 됩니다.

  5. 그런 다음 로그인 사용자가 필요한 다른 XMLHttpRequest를 만듭니다.현재 세션에 로그인한 사용자가 없으므로 401을 반환합니다.

업데이트: 8/14 Gand logout은 새로운 CSRF 토큰을 제공하지 않습니다.로그아웃 후에 일반적으로 발생하는 리다이렉트에서는 새로운 CSRF 토큰을 얻을 수 있습니다.

제 답변은 @Jimbo와 @Sija에서 많이 차용하고 있습니다만, 저는 Rails CSRF Protection + Angular.js: protect_from_forgular.js: POST에서 로그아웃을 하게 하고, 이것을 처음 했을 때 블로그에서 조금 상세하게 설명하고 있습니다.응용 프로그램컨트롤러에서는 csrf의 cookie를 설정하는 방법이 있습니다.

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 형식을 사용하고 있지만 이전 SO 솔루션의 코드를 사용하여 다음과 같은 이점을 얻을 수 있습니다.

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 청소의 일부이기도 합니다.이것은 다른 사람에게도 흥미로울지도 모릅니다.블로그 투고는 이쪽입니다.기타 변경 사항은 다음과 같습니다.

Action Controller로부터의 구제:무효 인증토큰: 동기화가 안 되면 사용자가 쿠키를 지우는 대신 응용 프로그램이 자동으로 수정됩니다.레일에 놓여 있는 상태에서는 어플리케이션 컨트롤러의 디폴트 설정은 다음과 같습니다.

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

또한 레이스 조건과 블로그 투고에서 자세히 코멘트한 Gande의 타임아웃 가능 모듈과의 상호 작용에 대해서도 약간의 문제가 있었습니다.요컨대 cookie_store가 아닌 active_record_store를 사용하는 것을 고려해 주십시오.또한 sign_in과 sign_out 액션에 가까운 병행 요구를 발행하는 것에 주의해야 합니다.

제 생각은 이렇습니다.

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 되어 CSRF가 반환될 됩니다.X-CSRF-Token ★★★★★★★★★★★★★★★★★」X-CSRF-Param머리글

소스를해 본 , 이 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★.sign_out_all_scopes로로 합니다.false그럼 Warden이 세션 전체를 클리어할 수 없게 되므로 CSRF 토큰은 로그아웃 사이에 유지됩니다.

문제 태커의 설계 관련 설명: https://github.com/plataformatec/devise/issues/2200

방금 레이아웃 파일에 추가했는데 작동했어요

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

이것을 application.js 파일에 포함했는지 확인합니다.

//=에는 jquery가 있습니다.

//=에는 jquery_ujs가 필요합니다.

그 이유는 기본적으로는 모든 Ajax 요구에 CSRF 토큰을 자동으로 설정하는 jquery-rails gem 이므로 이들 2개가 필요합니다.

제 경우 로그인 후 사용자 메뉴를 다시 그릴 필요가 있었습니다.이것으로 동작했습니다만, 같은 섹션(물론 페이지를 갱신하지 않고)에서 서버에 대한 모든 요구로 CSRF 인증 에러가 발생했습니다.js 뷰를 렌더링해야 했기 때문에 위의 솔루션이 작동하지 않았습니다.

제가 한 일은 Gand를 사용한 것입니다.

app/controllers/controller.displays

   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

그 후 controller #action에 메뉴를 다시 그려달라고 요청했습니다.Javascript에서 X-CSRF-Param과 X-CSRF-Token을 수정했습니다.

app/filename/redraw_user_menu.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 상황에 있는 사람에게 도움이 되었으면 합니다.

내 상황은 더 간단했다.제 경우, 제가 하고 싶었던 것은 이것뿐이었습니다.어떤 사람이 폼을 가진 화면에 앉아 있을 때 세션이 타임아웃(디바이스 타임아웃 가능 세션타임아웃)이 되면 보통 그 시점에서 Submit을 누르면 Gand는 로그인 화면으로 되돌립니다.글쎄, 난 그걸 원하지 않았어 왜냐면 그들은 양식 데이터를 다 잃어버렸거든.제출 폼을 잡기 위해 JavaScript를 사용합니다.Ajax는 사용자가 로그인하지 않았는지 여부를 판단하는 컨트롤러를 호출합니다.이 경우 사용자가 비밀번호를 재입력하는 폼을 올리고 Ajax 콜을 사용하여 재인증합니다(컨트롤러 내 bypass_sign_in).그러면 원래 제출 양식을 계속할 수 있습니다.

'위조로부터 보호'를 추가할 때까지 완벽하게 작동하고 있었어요

따라서 위의 답변에 따라 실제로 필요한 것은 컨트롤러에서 사용자를 다시 서명(bypass_sign_in)하는 것뿐입니다.이 경우 인스턴스 변수를 새로운 CSRF 토큰으로 설정하기만 하면 됩니다.

@new_csrf_token = form_authenticity_token

그 후 렌더링된 .js.erb에서 (이것도 XHR 콜이었으므로)

$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');

Voila. 갱신되지 않아 이전 토큰으로 고정되어 있던 폼 페이지에 사용자 서명에서 얻은 새 세션의 새 토큰이 들어 있습니다.

@syslog4bit의 코멘트에 응답합니다.이 에러가 발생했을 경우는, 다음과 같이 됩니다.

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

언급URL : https://stackoverflow.com/questions/11845500/rails-devise-authentication-csrf-issue

반응형