31

我正在努力理解 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(...)将返回 a Future,因此我不知道如何利用内置表单绑定功能和异步工具。

宣传异步方法固然很好,但我对框架的某些部分不能很好地配合它感到沮丧。如果验证必须同步完成,它似乎违背了异步方法的要点。我在使用Action组合时遇到了类似的问题 - 例如,与安全相关Action的会调用 ReactiveMongo。

谁能阐明我的理解力不足的地方?

4

5 回答 5

9

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

验证建立在play.api.data.validation.Constraint从验证值到存储函数的基础上ValidationResultValid或者Invalid,这里没有地方放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 而设计的。使其异步会使其更复杂且更难使用,因此它保持简单。让框架中的每一段代码都处理包装在Futures 中的数据是多余的。

如果您需要使用 ReactiveMongo 进行验证,您可以使用Await.result. ReactiveMongo 在任何地方都返回 Futures,您可以阻塞直到这些 Futures 完成以在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 的未来版本中实现它。

于 2013-02-18T07:38:05.530 回答
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.")
      }
    }
  }
}
于 2013-05-04T16:23:05.157 回答
3

表单验证意味着字段的语法验证,一一进行。如果一个文件没有通过验证,它可以被标记(例如带有消息的红色条)。

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

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

如果身份验证失败,BadRequest 和 Ok 都可以重新显示带有错误消息的表单。这些助手只指定响应的 HTTP 状态代码,独立于响应正文。

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

请查看您的 play 安装的 ZenTasks 示例。

于 2013-06-03T19:45:21.303 回答
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 ...)
      }
    }
  )
}
于 2015-02-03T22:05:23.080 回答
1

我在 theguardian 的 GH repo 上看到了他们如何以异步方式处理这种情况,同时仍然获得表单错误助手的支持。乍一看,他们似乎将表单错误存储在加密的 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))
}
于 2015-09-23T05:30:28.053 回答