2013-02-16 8 views
30

Naprawdę staram się zrozumieć asynchroniczne moce Play, ale znajduję wiele konfliktów w odniesieniu do miejsc, w których spotyka się asynchroniczne wywoływanie, oraz miejsc, w których struktura zdaje się spiskować przeciwko jego użyciu.Czy mogę przeprowadzić walidację formularza asynchronicznego w Play Framework 2.x (Scala)?

Podany przykład dotyczy sprawdzania poprawności formularza. Gra pozwala zdefiniować ograniczenia ad-hoc - zobacz to z dokumentacji:

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

Ładne i czyste. Jednakże, jeśli używam warstwy asynchronicznej dostępu do danych (np. ReactiveMongo), takie wywołanie do User.authenticate(...) zwróci Future i jestem w ten sposób w ciemności, w jaki sposób mogę wykorzystać moc obu wbudowanych wiążących formularzy funkcje i narzędzia asynchroniczne.

Dobrze i dobrze jest upublicznić podejście asynchroniczne, ale denerwuję się, że niektóre części schematu nie są z nim tak dobrze odtwarzane. Jeśli sprawdzanie poprawności musi być wykonywane synchronicznie, wydaje się, że pokonuje punkt podejścia asynchronicznego. Podobny problem wystąpił podczas korzystania z kompozycji Action - np. zabezpieczenie związane z numerem Action, które wywołałoby wywołanie ReactiveMongo.

Czy ktoś może rzucić światło na to, gdzie moje pojmowanie jest niewystarczające?

Odpowiedz

9

Tak, sprawdzanie poprawności w Play jest zaprojektowane synchronicznie. Myślę, że to dlatego, że założono, że przez większość czasu nie ma operacji we/wy w sprawdzaniu poprawności formularza: wartości pól są sprawdzane tylko pod względem rozmiaru, długości, dopasowania względem wyrażeń regularnych itp.

Walidacja jest tworzona przez play.api.data.validation.Constraint, która zapisuje funkcję z zatwierdzonej wartości do ValidationResult (albo Valid lub Invalid, tutaj nie ma miejsca na umieszczenie 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 tylko dodaje inny z funkcją ograniczenia zdefiniowane przez użytkownika.

Uważam, że Data Binding w Play nie jest przeznaczona do wykonywania operacji wejścia/wyjścia podczas sprawdzania poprawności. Uczynienie go asynchronicznym sprawiłoby, że byłby on bardziej skomplikowany i trudniejszy w użyciu, a więc utrzymany w prostocie. Sprawienie, by każdy fragment kodu w ramach pracy nad danymi zawiniętymi w Future był przesadą.

Jeśli potrzebujesz użyć sprawdzania poprawności z ReactiveMongo, możesz użyć Await.result. ReactiveMongo zwraca Futures wszędzie, a możesz zablokować do zakończenia tych kontraktów futures, aby uzyskać wynik wewnątrz funkcji verifying. Tak, zmarnuje wątek podczas wykonywania zapytania 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") 
    } 
} 

Może istnieje sposób, aby nie tracić wątek za pomocą continuations, nie próbował go.

Myślę, że dobrze jest przedyskutować to na liście mailingowej Play, być może wiele osób chce wykonać asynchroniczne operacje wejścia/wyjścia w powiązaniu danych Play (na przykład w celu sprawdzenia wartości w bazie danych), aby ktoś mógł zaimplementować go dla przyszłych wersji Grać.

+0

Jak mogę ustawić komunikat o poprawności dynamicznie? Wiadomość może być na przykład "Nieprawidłowa nazwa użytkownika lub hasło" lub "Usługa niedostępna teraz". Drugie pytanie brzmi: czy mogę uzyskać obiekt użytkownika w działaniu bez duplikowania żądania autoryzacji? – Artem

6

Ja też zmagałem się z tym. Realistyczne aplikacje zwykle mają konta i uwierzytelnianie użytkowników.Zamiast blokowania wątku, alternatywą byłoby uzyskać parametry z formularza i obsługiwać połączenia uwierzytelniania w samej metodzie kontrolera, coś jak to:

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

walidacja Formularz oznacza syntaktycznej poprawności pól, jeden po jeden. Jeśli pole nie przejdzie sprawdzania poprawności, można je oznaczyć (np. Czerwony pasek z komunikatem).

Uwierzytelnienie należy umieścić w treści akcji, która może znajdować się w bloku Async. To powinno być po wywołaniu bindFromRequest, więc nie wolno mi po walidacji, więc po każdym pole nie jest puste, itp

Na podstawie wyniku połączeń asynchronicznych (np. ReactiveMongo nazywa) wynik działania może być BadRequest lub Ok.

Zarówno przy BadRequest, jak i Ok można ponownie wyświetlić formularz z komunikatem o błędzie, jeśli uwierzytelnienie nie powiodło się. Pomocnicy ci określają tylko kod statusu HTTP odpowiedzi, niezależnie od treści odpowiedzi.

Byłoby eleganckim rozwiązaniem do uwierzytelniania z play.api.mvc.Security.Authenticated (lub pisania podobnego, dostosowanego do indywidualnych potrzeb kompozytora akcji) i korzystania z wiadomości o zasięgu Flash. W związku z tym użytkownik zawsze zostanie przekierowany na stronę logowania, jeśli nie jest uwierzytelniona, ale jeśli prześle formularz logowania z błędnymi danymi uwierzytelniającymi, obok przekierowania zostanie wyświetlony komunikat o błędzie.

Proszę spojrzeć na przykład ZenTasks z instalacji odtwarzacza.

1

To samo pytanie było asked na liście odtwarzania korespondencji z Johan Andrén odpowiadając:

bym przenieść rzeczywiste uwierzytelnianie z walidacji formularza i zrobić to w swoim działaniu i zamiast używać walidacji tylko dla walidacji wymagane pola itp coś takiego:

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

widziałem na The Guardian za GH repo jak radzą sobie ten scenariusz w sposób asynchroniczny, a jednocześnie mając wsparcie pomocników błędach postać z gry. Wygląda na to, że przechowują błędy formularzy w zaszyfrowanym pliku cookie w taki sposób, aby wyświetlać te błędy użytkownikowi po następnym przejściu na stronę logowania.

wydobywać: 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

Szyfrowanie przechodzi do niejawnej metody "toFlash", którą można znaleźć w pliku implicits.Forms.scala – dzv3

Powiązane problemy