2013-02-16 23 views
30

我正在努力瞭解Play的異步功能,但發現與異步調用適合的位置以及框架似乎與其使用密切相關的地方存在很多衝突。我可以在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。

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

回答

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

verifying只是增加了用戶定義函數的另一個約束。

所以我認爲Play中的數據綁定並不是爲了在驗證時進行I/O而設計的。使其異步將使它更復雜,更難以使用,所以它保持簡單。使框架中的每一段代碼都適用於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") 
    } 
} 

也許有辦法不浪費線程通過使用continuations,沒有嘗試過。

我認爲在Play郵件列表中討論這個問題很好,也許很多人想要在Play數據綁定中執行異步I/O(例如,用於檢查數據庫的值),所以有人可能會爲未來的版本玩。

+0

如何設置驗證信息動態地:

摘自?例如,消息可能是「無效的用戶名或密碼」或「現在服務不可用」。 第二個問題是我可以在沒有重複認證請求的情況下獲取User對象嗎? – Artem 2014-07-28 07:41:05

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。

如果身份驗證失敗,BadRequest和Ok都可以重新顯示帶有錯誤消息的表單。這些助手只指定響應的HTTP狀態代碼,獨立於響應主體。

這將是一個優雅的解決方案,使用play.api.mvc.Security.Authenticated(或編寫類似的自定義操作合成器)進行身份驗證,並使用Flash範圍消息。因此,如果用戶未通過身份驗證,用戶總是會被重定向到登錄頁面,但如果她提交的登錄表單提供的憑據不正確,則會顯示除重定向之外的錯誤消息。

請看看您的遊戲安裝的ZenTasks示例。

1

同樣的問題在與約翰Andrén播放郵件列表asked回答:

我會在你的動作移動的實際驗證表單驗證,做它,而不是和使用驗證只是爲了驗證必填字段等等。事情是這樣的:

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

我已經看到了衛報的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)) 
} 
+0

加密的東西進入「toFlash」隱式方法,可以在它們的文件中找到implicits.Forms.scala – dzv3 2015-09-23 06:04:39

相關問題