Могу ли я сделать асинхронную проверку формы в 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.
Может ли кто-нибудь пролить свет на то, где мое понимание не ослабевает?
Ответы
Ответ 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))
}