[개발일지] Play Framework 2.4 + AngularJS 간에 CORS 필터 허용하기

최근 드디어 회사에서 조금씩 Paperworks의 범위를 줄이고 개발에 많이 신경을 쓰고 있다. 아직 얼마 안되긴 했지만..

지지난주 미국에 다시 와서, 계속해서 Play! Framework를 보고 있다. 확실히 AngularJS를 해서 그런가, Functional Programming에 점차 익숙해져 간다. Java 8에서 Stream API를 사용해서 그런지 몰라도 map이나 flatmap, filter등의 개념이 손쉽게 느껴진다. 정말 3년 전 스칼라 공부할 때는 아무것도 모르고 때려쳤는데.. 반성에 반성하고, 역시 개발은 끝없는 공부라는 생각을 한다.

어쨌든, 오늘 삽질한 부분은 Play 2.4의 CORS 필터를 세팅하는 부분이다. 

https://www.playframework.com/documentation/2.4.x/CorsFilter

위와 같은 좋은 메뉴얼이 있어서 생각보다 간단할 줄 알았는데, 왠걸 AngularJS에서 전송하니 잘 안된다.

크롬 디버거로 잘 확인해보니, OPTIONS을 보내고 있더라. 그래서 또 찾아보니

https://gist.github.com/mitchwongho/78cf2ae0276847c9d332#file-application-scala

이런 내용이 있다. 즉

# Application Routes
# This file defines application routes (Higher priority routes first)
# ~~~~
GET	...
OPTIONS  /                           controllers.Application.options(path="")
OPTIONS  /*path                      controllers.Application.options(path)

위와 같이 routes파일 수정해주고,

package controllers

import play.Play
import play.api.http.HeaderNames
import play.api.mvc._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object Application extends Controller {

	...

	def options(path: String) = CorsAction {
		Action { request =>
			Ok.withHeaders(ACCESS_CONTROL_ALLOW_HEADERS -> Seq(AUTHORIZATION, CONTENT_TYPE, "Target-URL").mkString(","))
		}
	}
}

// Adds the CORS header
case class CorsAction[A](action: Action[A]) extends Action[A] {

	def apply(request: Request[A]): Future[Result] = {
		action(request).map(result => result.withHeaders(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN -> "*",
		HeaderNames.ALLOW -> "*",
		HeaderNames.ACCESS_CONTROL_ALLOW_METHODS -> "POST, GET, PUT, DELETE, OPTIONS",
		HeaderNames.ACCESS_CONTROL_ALLOW_HEADERS -> "Origin, X-Requested-With, Content-Type, Accept, Referer, User-Agent"
		))
	}

	lazy val parser = action.parser
}

Application.scala 에도 위와 같이 CORS 헤더를 추가해준다.

import play.api._
import play.api.http.HeaderNames
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object Global extends WithFilters(CorsFilter) with GlobalSettings {

  // called when a route is found, but it was not possible to bind the request parameters
  override def onBadRequest (request: RequestHeader, error: String) = Future.successful(
    BadRequest("Bad Request: " + error)
  )

}

object CorsFilter extends Filter {

  def apply (nextFilter: (RequestHeader) => Future[Result])(requestHeader: RequestHeader): Future[Result] = {

    nextFilter(requestHeader).map { result =>
      result.withHeaders(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN -> "*",
        HeaderNames.ALLOW -> "*",
        HeaderNames.ACCESS_CONTROL_ALLOW_METHODS -> "POST, GET, PUT, DELETE, OPTIONS",
        HeaderNames.ACCESS_CONTROL_ALLOW_HEADERS -> "Origin, X-Requested-With, Content-Type, Accept, Referer, User-Agent"
      )
    }
  }
}

그리고 Global.scala에 (이 파일은 그냥 루트에 있으면 된다.) 위와 같이 WithFilter를 먹이면 된다.

근데 웃긴건, 젤 위 플레이 공식 Tutorial을 따라했을때는 잘 안되다가, 위의 코드를 추가하니 코드 자체의 Rule을 따르는게 아니라 Application.conf의 룰을 따른다. 이건 좀 웃기더라.. 결국 그냥 "필터 먹인다" 정도만 알려주고, 세팅된 값은 application.conf을 참조해오는 것 같다. 그리고, Route의 저 OPTIONS / 이나 OPTIONS /*path는 가장 상위 순서로 라우팅을 해서, OPTIONS은 모두 Controllers.Application.options으로 들어간다.

당연히 build.sbt의 dependencies에 filters는 추가해 줘야 한다..

생각보다 간단히 해결하긴 했다. Spring Boots쓸때는 진짜 3주인가 걸렸는데.. 역시, 플레이 프레임워크는 RESTful구조에서는 최상인 것 같다.

이제 정말 실질적인 Front/Backend 분리를 했다. JSON객체 받는 부분도 성공하고.. 이제 남은건 보안 처리. 그래도 정말 강력한 플레이 프레임워크 덕분에, 혼자 개발할 맛이 난다는 ^^