Подтвердить что ты не робот

Могу ли я сделать асинхронную проверку формы в Play Framework 2.x(Scala)?

Я делаю настоящий толчок, чтобы понять асинхронные возможности игры, но обнаруживаю много конфликтов в отношении мест, где подходит асинхронный вызов, и места, где структура, похоже, замышляет против ее использования.

Пример, который я имею в отношении проверки формы. 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.

Может ли кто-нибудь пролить свет на то, где мое понимание не ослабевает?

4b9b3361

Ответ 1

Да, проверка в Play разработана синхронно. Я думаю, это потому, что предполагалось, что в большинстве случаев нет ввода-вывода при проверке формы: значения полей проверяются только на размер, длину, сопоставление с регулярным выражением и т.д.

Валидация построена поверх play.api.data.validation.Constraint, в которой функция хранится с подтвержденным значением до ValidationResult (либо Valid, либо 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 просто не предназначена для ввода-вывода во время проверки. Сделать его асинхронным было бы сложнее и сложнее в использовании, поэтому он оставался простым. Создание каждого фрагмента кода в каркасе для работы с данными, обернутыми в 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")
  }
}

Возможно, есть способ не тратить поток, используя продолжения, не пробовал его.

Я думаю, что хорошо обсуждать это в списке рассылки Play, возможно, многие люди хотят делать асинхронные операции ввода-вывода в привязке данных воспроизведения (например, для проверки значений по базе данных), поэтому кто-то может реализовать его для будущих версий Play.

Ответ 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

Проверка формы означает синтаксическую проверку полей один за другим. Если поданная не проходит проверку, она может быть отмечена (например, красная строка с сообщением).

Аутентификация должна быть помещена в тело действия, которое может быть в блоке Async. Он должен быть после вызова bindFromRequest, поэтому мне нужно после проверки, поэтому после того, как каждое поле не пустое и т.д.

На основании результатов асинхронных вызовов (например, вызовы ReactiveMongo) результатом действия может быть либо BadRequest, либо Ok.

Оба с BadRequest и Ok могут повторно отобразить форму с сообщением об ошибке, если аутентификация завершилась неудачно. Эти помощники определяют только код статуса HTTP ответа, независимо от тела ответа.

Было бы элегантным решением выполнить аутентификацию с помощью play.api.mvc.Security.Authenticated (или написать аналогичный персонализированный компоновщик действий) и использовать сообщения с флэш-памятью. Таким образом, пользователь всегда будет перенаправлен на страницу входа в систему, если она не аутентифицирована, но если она представит регистрационную форму с неправильными учетными данными, сообщение об ошибке будет показано помимо перенаправления.

Пожалуйста, посмотрите пример ZenTasks вашей установки игры.

Ответ 4

Тот же вопрос был спросил в списке рассылки Play с Йоханом Андреном, отвечая:

Я бы переместил фактическую аутентификацию из проверки формы и выполнил ее в своем действии вместо этого и использовал проверку только для проверки необходимых полей и т.д. Что-то вроде этого:

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

Я видел на ggu repo gguguian, как они обрабатывают этот сценарий по асинхронному способу, все еще поддерживая поддержку ошибок формы. По быстрому виду кажется, что они хранят ошибки формы в зашифрованном файле 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))
}