Play 2.6과 AngularJS에서 CSRF 설정 Scala
2017.07.07 15:30 EDIT
플레이 프레임워크(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 필터를 적용하지 않는 것도 가능하다. 자세한 것은 문서를 참고하라.
참고 : What's the point of the X-Requested-With header? - StackOverflow
https://stackoverflow.com/questions/17478731/whats-the-point-of-the-x-requested-with-header/22533680
방법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"
참고 : Cross Site Request Forgery (XSRF) Protection - AngularJS: API: $http
https://docs.angularjs.org/api/ng/service/$http
템플릿을 통해 설정하기
두 번째 방법은 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)
참고 : Using AngularJs in order to retrieve a header from the response and set it on all requests - StackOverflow
https://stackoverflow.com/questions/27332717/using-angularjs-in-order-to-retrieve-a-header-from-the-response-and-set-it-on-al