Play 2.6과 AngularJS에서 CSRF 설정

플레이 프레임워크(Play Framework) 2.6 부터는 CSRF 필터가 기본으로 활성화되어 있다. 다시 말해 서버에서 CSRF 토큰을 감지하지 못할 경우 POST/PUT 등의 요청이 403 Forbidden 오류로 거부될 수 있다는 이야기이다. 서버 요청에 플레이에서 제공하는 헬퍼를 템플릿에 사용해왔다면 필드 하나를 추가해주면 그만이지만, 필자처럼 앵귤러(Angular) JS로 XHR을 이용한 통신을 주로 사용했을 경우 이 필터를 통과하거나 우회할 방법을 찾아야 한다.

방법 1. XHR 요청에 대해 CSRF 토큰 검사하지 않기

X-Requested-With 헤더는 표준은 아니지만 JQuery 등의 XHR 요청에 널리 사용되고 있다. CORS 설정을 통해 타 도메인의 스크립트로 이런 요청을 보내지 않는다는 것을 전제로, X-Requested-With 헤더가 있는 요청을 신뢰하도록 플레이를 설정할 수 있다. 다음은 플레이 문서에 나온 예시이다. 이와 더불어 3번째 행은 Csrf-Token 헤더가 nocheck일 경우 또한 신뢰하도록 설정하고 있다.

play.filters.csrf.header.bypassHeaders {
  X-Requested-With = "*"
  Csrf-Token = "nocheck"
}

이렇게 헤더 단위가 아니라 특정 route에 대해서만 CSRF 필터를 적용하지 않는 것도 가능하다. 자세한 것은 문서를 참고하라.

방법2. AngularJS $http 모듈의 요청에 CSRF 설정하기

쿠키를 통해 헤더 설정하기

첫 번째 방법은 쿠키를 이용해 CSRF 토큰을 설정하는 것이다. 서버에서 지정된 이름으로 쿠키를 발행하면, $http 모듈이 자동으로 CSRF 토큰으로 인식해 헤더를 설정한다. 다만, 이 방법을 쓰려면 일단 플레이 프레임워크에서 CSRF 토큰을 세션이 아닌 쿠키에 저장하도록 설정 파일을 수정해야 한다.

# AngularJS가 인식하는 기본 쿠키명
play.filters.csrf.cookie.name = "XSRF-TOKEN"

다음으로 CSRF 쿠키는 성능상의 이유로 항상 발행되지 않기 때문에, 발행할 라우트 메서드에서 CSRF.GetToken을 한번 호출해주어야한다. 다음 코드는 메뉴얼에도 나온 내용을 응용한 것이지만, 다소 이상해보이기는 한다.

def home = Action { implicit request =>
  accessToken // request is passed implicitly to accessToken
  Ok(html.index())
}

def accessToken(implicit request: Request[_]) = {
  val token = CSRF.getToken // request is passed implicitly to CSRF.getToken
}

이제 브라우저에서 home 액션에 접근하면 개발 도구 등을 통해 Set-Cookie 헤더로 XSRF-TOKEN이 쿠키에 설정되는 것을 확인할 수 있다. 앵귤러JS의 $http 모듈은 이 쿠키를 읽어들인 후 서버의 헤더로 전달하는데, 이 헤더명의 기본값은 X-XSRF-TOKEN으로 플레이에서 인식할 수 없기 때문에 바꾸어주어야 한다.

$httpProvider.defaults.xsrfHeaderName = "Csrf-Token"

템플릿을 통해 설정하기

두 번째 방법은 HTML을 통해 CSRF 토큰을 자바스크립트로 전달하는 것이다. 이 방법은 다른 서버측 설정이 필요없다는 점에서 좀더 플레이 프레임워크 친화적(?)으로 보이지만, 클라이언트 작업이 좀더 번잡하다. 우선 템플릿 상단에 아래와 같이 선언한다.

@import play.filters.csrf._
@()(implicit request: RequestHeader)
...

암묵적인 RequestHeader는 CSRF 값을 얻기 위해서 필요하다. 다음으로 앵귤러JS의 $httpProvider를 다음과 같이 설정한다.

$httpProvider.defaults.headers.common['Csrf-Token'] = "@CSRF.getToken.get.value";

이렇게 하면 해당 페이지의 $http를 이용한 모든 요청에 Csrf-Token이라는 헤더가 삽입된다. 이 헤더가 플레이 프레임워크에서 기본으로 인식하는 헤더이기 때문에 다른 설정은 필요없다.
만일 자바스크립트 파일이 Twirl 템플릿이 아니라면 위 값을 전역 변수나 다른 방법으로 앵귤러JS로 전달해야 할 것이다.
스칼라JS 바인딩(Scalajs-angular)을 사용한다면 설정은 다음과 같을 것이다. (0.8-SNAPSHOT 기준)
보면 알겠지만 headers 퍼서드는 더 개선될 여지가 있다. 참여를 기다린다 :-)

// 전제 : csrfToken이라는 String 변수에 토큰이 할당되어 있음
val headers = httpProvider.defaults.headers.asInstanceOf[Dictionary[Dictionary[String]]]
    headers("common") += ("Csrf-Token" -> csrfToken)
Tag :
, ,

Leave Comments