【问题标题】:Can I do async form validation in Play Framework 2.x (Scala)?我可以在 Play Framework 2.x (Scala) 中进行异步表单验证吗?
【发布时间】:2013-02-16 17:30:18
【问题描述】:

我正在努力理解 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。

任何人都可以阐明我的理解力不足的地方吗?

【问题讨论】:

    标签: asynchronous playframework-2.0


    【解决方案1】:

    是的,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 而设计的。使其异步会使其更复杂且更难使用,因此它保持简单。让框架中的每一段代码都可以处理 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 的未来版本中实现它.

    【讨论】:

    • 如何动态设置验证信息?例如,消息可能是“无效的用户名或密码”或“服务现在不可用”。第二个问题是我可以在没有重复 auth-request 的情况下获取 User 对象吗?
    【解决方案2】:

    我也一直在为此苦苦挣扎。现实的应用程序通常会有某种用户帐户和身份验证。除了阻塞线程,另一种方法是从表单中获取参数并在控制器方法本身中处理身份验证调用,如下所示:

    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。

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

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

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

      【讨论】:

        【解决方案4】:

        同样的问题是在 Play 邮件列表中的 asked,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 ...)
              }
            }
          )
        }
        

        【讨论】:

          【解决方案5】:

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

          【讨论】:

          • 加密的东西进入“toFlash”隐式方法,可以在他们的文件implicits.Forms.scala中找到
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-04
          • 1970-01-01
          • 2023-04-01
          • 1970-01-01
          • 2017-03-02
          • 1970-01-01
          相关资源
          最近更新 更多