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

30

我正在努力理解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组合(例如,调用ReactiveMongo的安全相关Action)时,我遇到了类似的问题。

有人能否解释一下我的理解哪里有问题?

5个回答

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)
}

验证只是使用用户定义的函数增加了另一个约束条件。

因此,我认为在Play中的数据绑定不适合在验证期间进行I/O操作。将其异步化会使其更加复杂和难以使用,因此保持简单。让框架中的每个代码片段都在包装在Future中的数据上工作是过度的。

如果您需要使用ReactiveMongo进行验证,则可以使用Await.result。ReactiveMongo在任何地方都返回Futures,并且您可以阻塞直到这些Futures完成以在验证函数内获取结果。是的,在运行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可能会有人实现它。

我该如何动态设置验证消息?例如,消息可能是“用户名或密码无效”或“服务当前不可用”。第二个问题是,在Action中是否可以获取User对象而不重复进行身份验证请求? - Artem

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.")
      }
    }
  }
}

3
验证指的是对每个字段进行语法验证,逐一检查。 如果某个字段未能通过验证,则可以标记该字段(例如:带有红色消息的提示栏)。
身份验证应该放在操作的主体中,这可能会在异步块中进行。 它应该在bindFromRequest调用之后,因此必须在验证之后进行,以确保每个字段都不为空等。
基于异步调用的结果(例如:ReactiveMongo调用),操作的结果可以是BadRequest或Ok。
BadaRequest和Ok都可以在身份验证失败时重新显示带有错误消息的表单。这些辅助程序仅指定响应的HTTP状态码,而与响应正文无关。
使用play.api.mvc.Security.Authenticated(或编写类似的自定义操作组合器)进行身份验证,并使用Flash作用域消息,将是一种优雅的解决方案。因此,如果用户未经过身份验证,她将始终被重定向到登录页面,但是如果她提交了具有错误凭据的登录表单,则会在重定向之外显示错误消息。
请查看您安装的Play软件的ZenTasks示例。

2
相同的问题在Play邮件列表中被问到,Johan André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 ...)
      }
    }
  )
}

1
我在theguardian的GH代码库中看到了他们如何异步处理这种情况,同时仍然得到来自play的表单错误助手的支持。从快速查看中,似乎他们将表单错误存储在一个加密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))
}

加密相关的内容放入“toFlash”隐式方法中,该方法可以在文件implicits.Forms.scala中找到。 - Diego Zacarias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接