Scala/Play framework

Play Framework 2.5. Action, Security Object (로그인/보안)

partner_jun 2017. 4. 14. 15:45

대부분의 웹 사이트에는 인증된 유저만 사용 가능한 '보안' 기능을 구현한다. Spring Framework에서는 AOP를 기반으로 한 Spring Security을 통해 보안뿐 아니라 다양한 기능을 지원한다. Play Framework는 Spring Security같은 자체 라이브러리는 없지만 Security Object나 ActionBuilder를 이용해 간단하게 구현할 수 있다. 하지만 session이나 header의 토큰 인증을 넘어 더 다양한 기능이나 Spring Security에서처럼 URL(routes)에 기반한 인증이 필요하다면 Deadbolt2같은 외부 라이브러리를 쓰는 것을 고려해야 한다.




1. Controller의 Action

먼저 컨트롤러에서 사용되는 Action Object에 대해 알아보자. 

@Singleton
class SimpleController @Inject()() extends Controller{

def index: Action[AnyContent] = Action { implicit request =>
Ok(views.html.index())
}

}

Controller에서 사용되는 Action Object



Play Framework의 Controller에서 사용되는 Action 함수는 여러가지 구현을 가지고 있다. 그 중 가장 많이 사용되는 위와 같은 구문에 해당하는 Action 오브젝트의 apply 함수를 보자.

/**
* Constructs an `Action` with default content.
*
* For example:
* {{{
* val echo = Action { request =>
* Ok("Got request [" + request + "]")
* }
* }}}
*
* @param block the action code
* @return an action
*/
final def apply(block: R[AnyContent] => Result): Action[AnyContent] = apply(BodyParsers.parse.default)(block)

R은 Request를 상속받아 구현한 타입을 포함한다(공변적).


파라미터로 Request를 입력받아 Result를 반환하는 함수를 전달받고(call-by-name), BodyParser.parse.default 메소드를 이용해 Request의 Method를 확인한 후 파라미터로 전달받은 '동작' 함수를 실행한다는 것을 알 수 있다. 결국 컨트롤러에 작성하는 함수는 Action.apply 함수에 전달될 함수라는 것이다. 


따라서 파라미터로 넘긴 함수를 다시 Action.apply 함수에 전달하는, Action을 감싸는 함수도 만들 수 있다.

def actionWrapper(block: Request[AnyContent] => Result): Action[AnyContent] = {
Action {req =>
block(req)
}
}

def index: Action[AnyContent] = actionWrapper { implicit request =>
Ok(views.html.index())
}

actionWrapper 함수는 전달받은 '동작' 함수를 Action.apply 함수에 다시 전달한다.


더 나아가, Request에서 특정 값을 체크하는 함수도 전달 할 수 있다.

def actionWrapper2(check: Request[AnyContent] => Boolean)
(action: Request[AnyContent] => Result): Action[AnyContent] = {
Action {req =>
if(check(req)) // 함수 check는 Request에서 "name" 쿼리가 있으면 true, 없으면 false를 반환한다.
action(req)
else
Results.Unauthorized
}
}

def index2 = actionWrapper2{ req =>
val name = req.getQueryString("name")
if(name.isDefined) true else false
}{ req =>
Ok(req.getQueryString("name").get)
}

actionWrapper2 함수는 전달받은 check함수를 이용해 Request를 체크한 후, 

결과에 따라 두 번째로 전달받는 action 함수를 실행할지 여부를 선택한다.


이런 방법을 기반으로 Action.apply를 감싸는 함수를 만들어 Request의 session 혹은 cookie, header 등에 특정 값을 가지고 있는 '로그인' 상태일 때에만 특정 페이지로 접근하도록 하는 구현을 할 수 있다. 처음부터 직접 구현해도 되겠지만, Play Framework에서는 구현을 조금 더 쉽게 해주는 Security Object를 지원하고 있다.




2. ActionBuilder, WrappedRequest

Security Object를 이용해 만들기 전에, Action Object를 감싸는 방법이 아닌 ActionBuilder를 이용해 새로운 Action을 만들어보자. ActionBuilder를 이용해 만든 Custom Action 자체는 Action을 감싸 만든 Action과 크게 다를 것은 없지만 Request를 상속하거나 감싸 만든 Custom Request를 처리할 수 있고, Action.async 메소드같은 Action 자체의 기능을 활용할 수 있다는 장점이 있다.



먼저 WrappedRequest를 상속해 Request를 감싸는 CustomRequest를 구현한다.(Request를 상속해 새로운 Request를 만들어도 된다)

import play.api.mvc.{Request, WrappedRequest}

class CustomRequest[A](val name: String, val request: Request[A]) extends WrappedRequest[A](request)
// val 키워드를 이용해 public field로 만든다.


그 다음, CustomRequest를 처리하는 CustomAction을 구현한다. 물론 CustomRequest가 아닌 일반적인 Request를 처리할 수도 있다.

import play.api.mvc.{ActionBuilder, Request, Result, Results}

import scala.concurrent.ExecutionContext.Implicits._
import scala.concurrent.Future

object CustomAction extends ActionBuilder[CustomRequest]{

override def invokeBlock[A](request: Request[A], block: (CustomRequest[A]) => Future[Result]): Future[Result] = {
val name = request.getQueryString("name")
if(name.isDefined) {
// request.getQueryString("name")이 존재하면
// 그 값과 Request를 CustomRequest로 만들어 코드블럭에 전달한다.
block(new CustomRequest[A](name.get, request))
}else
Future { Results.Ok("name is not defined") }
}

}

ActionBuilder Trait의 invokeBlock을 구현한다.


이렇게 만든 CustomAction은 컨트롤러에서 아주 간단하게 사용할 수 있다.

def index = CustomAction { req => // req는 CustomRequest 객체
// req.name <- request.getQueryString("name").get
// req.request <- Request
Ok(req.name)
}

CustomAction에 전달되는 함수는 CustomRequest를 사용하는 객체다.




3. Security Object

Security.scala의 구현을 보면 많은 함수들이 구현되어 있다. 하지만 대부분은 session.username의 여부를 확인하는 구현이거나 이미 살펴본 ActionBuilder와 WrappedRequest를 활용한 AuthenticatedBuilder 클래스의 구현이다. 그래서 가장 일반적이고 활용도가 높은 Security.Authenticated 함수만 살펴보려고 한다.

def Authenticated[A](
userinfo: RequestHeader => Option[A],
onUnauthorized: RequestHeader => Result)(action: A => EssentialAction): EssentialAction = {

EssentialAction { request =>
userinfo(request).map { user =>
action(user)(request)
}.getOrElse {
Accumulator.done(onUnauthorized(request))
}
}

}

Security.Authenticated 함수의 구현. Accmulator를 직접 사용하는 EssentialAction이라는 점이 다르다. 


Security Object의 Authenticated 함수는 Request를 체크해 Option 값을 반환하는 함수반환된 값이 None일 경우 실행할 함수, Some일때 실행할 함수 세 개의 함수를 파라미터로 전달받는다. 이것 역시 코드를 보면 이해하기 쉽다.

trait SimpleAuthentication {

// Controller에서 Action 대신 사용할 함수
def withAuth(block: (String, Request[AnyContent]) => Result): EssentialAction = {
Security.Authenticated(authChecker, onUnauthorized) { name =>
Action(request => block(name, request))
}
}

// Request를 체크해 Option값을 반환하는 함수
private def authChecker(req: RequestHeader): Option[String] = {
req.getQueryString("name")
}

// None일 경우 실행되는 함수
private def onUnauthorized(req: RequestHeader) = {
Results.Ok("Name is not defined")
}

}

컨트롤러에 상속해 사용할 수 있게 Trait으로 구현했다. Object로 구현해도 상관없다.


이렇게 구현한 트레잇은 컨트롤러에 상속해 사용한다.

@Singleton
class SimpleController @Inject()() extends Controller with SimpleAuthentication {

def index = withAuth{ (name, request) =>
Ok(name)
}

}



파라미터 두 개를 한번에 입력받는 함수가 아니라 파라미터를 한 개씩 입력받는 커링된 함수로 구현 할 수도 있다.

def withAuth2(block: String => Request[AnyContent] => Result): EssentialAction = {
Security.Authenticated(authChecker, onUnauthorized) { name =>
Action(request => block(name)(request))
}
}
def index = withAuth2{ name => request =>
Ok(name)
}