我可以在Play Framework 2.x(Scala)中进行异步表单验证吗?

时间:2013-02-16 17:30:19

标签: asynchronous playframework-2.0

我正在努力去理解Play的异步功能,但是在异步调用适合的地方发现了很多冲突,并且框架似乎与它的使用密切相关。

我的示例与表单验证有关。 Play允许定义临时约束 - 请参阅文档:

val loginForm = Form(
  tuple(
    "email" -> email,
    "password" -> text
  ) verifying("Invalid user name or password", fields => fields match { 
      case (e, p) => User.authenticate(e,p).isDefined 
  })
)

干净整洁。但是,如果我使用完全异步数据访问层(例如ReactiveMongo),那么对User.authenticate(...)的调用将返回Future,因此我对如何利用内置表单绑定功能和异步工具的强大功能。

宣传异步方法一切都很好,但我对框架的某些部分不能很好地发挥作用感到沮丧。如果验证必须同步进行,那么它似乎打败了异步方法。我在使用Action作品时遇到了类似的问题 - 例如与安全相关的Action,可以调用ReactiveMongo。

任何人都可以了解我的理解力不足之处吗?

5 个答案:

答案 0 :(得分:9)

是的,Play中的验证是同步设计的。我认为这是因为假设大多数时候表单验证中没有I / O:只检查字段值的大小,长度,匹配正则表达式等。

验证是基于play.api.data.validation.Constraint构建的,它将函数从验证值存储到ValidationResultValidInvalid,没有地方可以将Future放在这里)。

/**
 * A form constraint.
 *
 * @tparam T type of values handled by this constraint
 * @param name the constraint name, to be displayed to final user
 * @param args the message arguments, to format the constraint name
 * @param f the validation function
 */
case class Constraint[-T](name: Option[String], args: Seq[Any])(f: (T => ValidationResult)) {

  /**
   * Run the constraint validation.
   *
   * @param t the value to validate
   * @return the validation result
   */
  def apply(t: T): ValidationResult = f(t)
}

verifying只是添加了一个用户定义函数的约束。

所以我认为Play中的数据绑定不是为验证时进行I / O而设计的。使它变得异步将使它更复杂和更难使用,因此它保持简单。使框架中的每一段代码都处理Future中包含的数据是过度的。

如果您需要对ReactiveMongo使用验证,则可以使用Await.result。 ReactiveMongo在任何地方返回期货,您可以阻止这些期货完成以获得verifying函数内的结果。是的,当MongoDB查询运行时,它将浪费一个线程。

object Application extends Controller {
  def checkUser(e:String, p:String):Boolean = {
    // ... construct cursor, etc
    val result = cursor.toList().map( _.length != 0)

    Await.result(result, 5 seconds)
  }

  val loginForm = Form(
    tuple(
      "email" -> email,
      "password" -> text
    ) verifying("Invalid user name or password", fields => fields match { 
      case (e, p) => checkUser(e, p)
    })
  )

  def index = Action { implicit request =>
    if (loginForm.bindFromRequest.hasErrors) 
      Ok("Invalid user name")
    else
      Ok("Login ok")
  }
}

也许有办法不使用continuations来浪费线程,而不是尝试它。

我认为在Play邮件列表中讨论这个很好,也许很多人想在Play数据绑定中进行异步I / O(例如,用于检查数据库的值),所以有人可能会为将来的Play版本实现它

答案 1 :(得分:6)

我也一直在努力解决这个问题。现实的应用程序通常会有某种用户帐户和身份验证。除了阻塞线程之外,另一种方法是从表单中获取参数并在控制器方法本身中处理身份验证调用,如下所示:

def authenticate = Action { implicit request =>
  Async {
    val (username, password) = loginForm.bindFromRequest.get
    User.authenticate(username, password).map { user =>
      user match {
        case Some(u: User) => Redirect(routes.Application.index).withSession("username" -> username)
        case None => Redirect(routes.Application.login).withNewSession.flashing("Login Failed" -> "Invalid username or password.")
      }
    }
  }
}

答案 2 :(得分:3)

表单验证意味着逐个语句的语法验证。 如果提交的文件未通过验证,则可以对其进行标记(例如带有消息的红条)。

身份验证应放在操作的主体中,该操作可能位于异步块中。 它应该在bindFromRequest调用之后,因此在验证之后必须有我,所以在每个字段不为空之后等等。

根据异步调用的结果(例如,ReactiveMongo调用),操作的结果可以是BadRequest或Ok。

如果身份验证失败,使用BadRequest和Ok都可以重新显示带有错误消息的表单。这些帮助程序仅指定响应的HTTP状态代码,与响应主体无关。

使用play.api.mvc.Security.Authenticated进行身份验证(或编写类似的自定义动作合成器)并使用Flash作用域消息将是一个优雅的解决方案。因此,如果用户未经过身份验证,则始终会被重定向到登录页面,但如果她使用错误的凭据提交登录表单,则会在重定向之外显示错误消息。

请查看您的游戏安装的ZenTasks示例。

答案 3 :(得分:1)

同样的问题是Play邮件列表中的asked,JohanAndrén回复:

我将实际身份验证移出表单验证并在您的操作中执行,并仅将验证用于验证必填字段等。如下所示:

val loginForm = Form(
  tuple(
    "email" -> email,
    "password" -> text
  )
)

def authenticate = Action { implicit request =>
  loginForm.bindFromRequest.fold(
    formWithErrors => BadRequest(html.login(formWithErrors)),
    auth => Async {
      User.authenticate(auth._1, auth._2).map { maybeUser =>
        maybeUser.map(user => gotoLoginSucceeded(user.get.id))
        .getOrElse(... failed login page ...)
      }
    }
  )
}

答案 4 :(得分:1)

我已经在theguardian的GH回购中看到他们如何以异步方式处理这种情况,同时仍然得到来自游戏的表单错误助手的支持。从快速看,似乎他们将表单错误存储在加密的cookie中,以便在用户下次登录页面时将这些错误显示给用户。

摘自:https://github.com/guardian/facia-tool/blob/9ec455804edbd104861117d477de9a0565776767/identity/app/controllers/ReauthenticationController.scala

def processForm = authenticatedActions.authActionWithUser.async { implicit request =>
  val idRequest = idRequestParser(request)
  val boundForm = formWithConstraints.bindFromRequest
  val verifiedReturnUrlAsOpt = returnUrlVerifier.getVerifiedReturnUrl(request)

  def onError(formWithErrors: Form[String]): Future[Result] = {
    logger.info("Invalid reauthentication form submission")
    Future.successful {
      redirectToSigninPage(formWithErrors, verifiedReturnUrlAsOpt)
    }
  }

  def onSuccess(password: String): Future[Result] = {
      logger.trace("reauthenticating with ID API")
      val persistent = request.user.auth match {
        case ScGuU(_, v) => v.isPersistent
        case _ => false
      }
      val auth = EmailPassword(request.user.primaryEmailAddress, password, idRequest.clientIp)
      val authResponse = api.authBrowser(auth, idRequest.trackingData, Some(persistent))

      signInService.getCookies(authResponse, persistent) map {
        case Left(errors) =>
          logger.error(errors.toString())
          logger.info(s"Reauthentication failed for user, ${errors.toString()}")
          val formWithErrors = errors.foldLeft(boundForm) { (formFold, error) =>
            val errorMessage =
              if ("Invalid email or password" == error.message) Messages("error.login")
              else error.description
            formFold.withError(error.context.getOrElse(""), errorMessage)
          }

          redirectToSigninPage(formWithErrors, verifiedReturnUrlAsOpt)

        case Right(responseCookies) =>
          logger.trace("Logging user in")
          SeeOther(verifiedReturnUrlAsOpt.getOrElse(returnUrlVerifier.defaultReturnUrl))
            .withCookies(responseCookies:_*)
      }
  }

  boundForm.fold[Future[Result]](onError, onSuccess)
}

def redirectToSigninPage(formWithErrors: Form[String], returnUrl: Option[String]): Result = {
  NoCache(SeeOther(routes.ReauthenticationController.renderForm(returnUrl).url).flashing(clearPassword(formWithErrors).toFlash))
}