首页 > 解决方案 > Vaadin LoginForm - signaling when user passed or failed authentication

问题描述

I understand that in using the Login component of Vaadin 14, I must call addLoginListener to register a listener of my own that implements ComponentEventListener<AbstractLogin.LoginEvent>. In my implementing code, I can call the LoginEvent::getUsername and LoginEvent::getPassword methods to obtain the text values entered by the user. My listener code then determines if these credentials are correct.

➥ By what mechanism does my listener code communicate back to my LoginForm the results of the authentication check?

If the authentication check succeeded, I need my LoginForm to close and let navigation continue on to the intended route. If the authentication failed, I need the LoginForm to inform the user about the failure, and ask user to re-enter username and password. How can my listener code tell the LoginForm what to do next?

标签: authenticationvaadinevent-listenervaadin-flow

解决方案


LoginForm仅支持其按钮的事件

LoginForm可以为两种事件注册监听器:

  • 用户单击“登录”按钮
  • 用户单击“忘记密码”按钮
    (可能显示也可能不显示)

这些对于我们检测用户是否成功完成登录尝试都没有用。注册“登录”按钮单击的任何方法都将决定用户的成功或失败。正是方法需要发出成功登录尝试的信号。

作为一个Component,它具有为其他类型的事件注册侦听LoginForm器的内置支持。不幸的是,该支持的范围仅限于. 所以事件监听器支持不能扩展到我们的 web 应用程序类,因为它们不是 Vaadin 包的一部分(超出范围)。protected

不使用路由

我尝试使用 Vaadin Flow 的路由功能,但未能成功。Vaadin 14.0.2 中的路由行为似乎存在一些严重问题。再加上我对路由的无知,这对我来说是不行的。相反,我MainView使用单个 URL(根“”)管理我内部的所有内容。

子类LoginForm

我不知道这是否是最好的方法,但我选择创建一个LoginForm被调用的子类AuthenticateView

➥ 在那个子类上,我添加了一个嵌套接口,AuthenticationPassedObserver定义了一个方法authenticationPassed。这是我的回调尝试,以通知我MainView用户尝试登录的成功。这是用户通过身份验证时如何发出信号的问题的具体解决方案:使调用布局实现在LoginForm子类上定义的接口,在用户登录尝试成功后调用单个方法。

失败不是一种选择

请注意,我们不关心失败。只要用户成功或关闭 Web 浏览器窗口/选项卡,我们只需将LoginForm子类显示为我们的内容,从而终止会话。MainView如果您担心黑客无休止地尝试登录,您可能希望您的子类LoginForm跟踪重复尝试并做出相应反应。但是 Vaadin Web 应用程序的性质使得这种攻击不太可能发生。

打回来

这是同步回调的嵌套接口。

    interface AuthenticationPassedObserver
    {
        void authenticationPassed ( );
    }

此外,我定义了一个接口Authenticator来确定用户名和密码凭据是否有效。

package work.basil.ticktock.backend.auth;

import java.util.Optional;

public interface Authenticator
{
    public Optional <User> authenticate( String username , String password ) ;

    public void rememberUser ( User user );  // Collecting.
    public void forgetUser (  );  // Dropping user, if any, terminating their status as  authenticated.
    public boolean userIsAuthenticated () ;  // Retrieving.
    public Optional<User> fetchUser () ;  // Retrieving.

}

现在我有一个该接口的抽象实现来处理存储User我定义的代表每个人的登录的类的对象的杂务。

package work.basil.ticktock.backend.auth;

import com.vaadin.flow.server.VaadinSession;

import java.util.Objects;
import java.util.Optional;

public abstract class AuthenticatorAbstract implements Authenticator
{
    @Override
    public void rememberUser ( User user ) {
        Objects.requireNonNull( user );
        VaadinSession.getCurrent().setAttribute( User.class, user  ) ;
    }

    @Override
    public void forgetUser() {
        VaadinSession.getCurrent().setAttribute( User.class, null  ) ; // Passing NULL clears the stored value.
    }

    @Override
    public boolean userIsAuthenticated ( )
    {
        Optional<User> optionalUser = this.fetchUser();
        return optionalUser.isPresent() ;
    }

    @Override
    public Optional <User> fetchUser ()
    {

        Object value = VaadinSession.getCurrent().getAttribute( User.class ); // Lookup into key-value store.
        return Optional.ofNullable( ( User ) value );
    }

}

也许我会将这些方法移到default界面上。但现在已经足够好了。

我写了一些实现。一对夫妇用于初始设计:一个总是不经检查就接受证书,另一只总是不经检查就拒绝证书。另一种实现是真实的,在用户数据库中进行查找。

验证器返回一个Optional< User >,这意味着如果凭证失败,则可选为空,如果凭证通过,则包含一个User对象。

这里总是失败:

package work.basil.ticktock.backend.auth;

import work.basil.ticktock.ui.AuthenticateView;

import java.util.Optional;

final public class AuthenticatorAlwaysFlunks  extends AuthenticatorAbstract
{
    public Optional <User> authenticate( String username , String password ) {
        User user = null ;
        return Optional.ofNullable ( user  );
    }
}

......并且总是通过:

package work.basil.ticktock.backend.auth;

import java.util.Optional;
import java.util.UUID;

public class AuthenticatorAlwaysPasses extends AuthenticatorAbstract
{
    public Optional <User> authenticate( String username , String password ) {
        User user = new User( UUID.randomUUID() , "username", "Bo" , "Gus");
        this.rememberUser( user );
        return Optional.ofNullable ( user  );
    }
}

顺便说一句,我并不是想在这里使用回调。几个压力点让我想到了这个设计。

  • 确实想让LoginView子类不知道是谁在调用它。一方面,我可能有一天会让路由工作,或者实现一些导航器框架,然后显示LoginForm子类的更大上下文可能会改变。所以我不想MainView在产生这个子类对象的对象上硬编码对方法的调用LoginForm
  • 我更喜欢将方法引用传递给我的LoginForm子类的构造函数的更简单的方法。然后我可以省略整个定义嵌套接口的事情。像这样的东西:AuthenticateView authView = new AuthenticateView( this::authenticationPassed , authenticator );。但是我对lambdas不够了解,不知道如何定义我自己的方法引用参数。

MainView- 用于显示和响应LoginView子类的控制器

最后,这是我早期尝试修改MainViewVaadin Starter 项目给我的类的实验。

注意如何MainView实现嵌套回调接口AuthenticateView.AuthenticationPassedObserver。当用户成功完成登录后,将调用authenticationPassed此处的方法。该方法从 的内容显示中MainView清除子类,并安装当前用户有权查看的常规应用内容。LoginFormMainView

还要注意注释:

  • 尽管用户单击浏览器刷新按钮,但版本 14 中的@PreserveOnRefresh新 Vaadin Flow 仍可保持内容活跃。这可能有助于登录过程,我想我还没有考虑清楚。
  • 另外,请注意@PWA. 我目前不需要渐进式网络应用程序功能。但是在 Vaadin 14.0.2 中删除该注释似乎会导致对包含我们的 Web 浏览器窗口/选项卡内容的对象进行更虚假的替换UI,因此我将保留该行。
  • 虽然我没有使用 Flow 中的路由功能,但我不知道如何禁用该功能。所以我把它留@Route ( "" )在原地。

package work.basil.ticktock.ui;

import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.PreserveOnRefresh;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;
import work.basil.ticktock.backend.auth.Authenticator;
import work.basil.ticktock.backend.auth.AuthenticatorAlwaysPasses;

import java.time.Instant;

/**
 * The main view of the web app.
 */
@PageTitle ( "TickTock" )
@PreserveOnRefresh
@Route ( "" )
@PWA ( name = "Project Base for Vaadin", shortName = "Project Base" )
public class MainView extends VerticalLayout implements AuthenticateView.AuthenticationPassedObserver
{

    // Constructor
    public MainView ( )
    {
        System.out.println( "BASIL - MainView constructor. " + Instant.now() );
        this.display();
    }

    protected void display ( )
    {
        System.out.println( "BASIL - MainView::display. " + Instant.now() );
        this.removeAll();

        // If user is authenticated already, display initial view.
        Authenticator authenticator = new AuthenticatorAlwaysPasses();
        if ( authenticator.userIsAuthenticated() )
        {
            this.displayContentPanel();
        } else
        { // Else user is not yet authenticated, so prompt user for login.
            this.displayAuthenticatePanel(authenticator);
        }
    }

    private void displayContentPanel ( )
    {
        System.out.println( "BASIL - MainView::displayContentPanel. " + Instant.now() );
        // Widgets.
        ChronListingView view = new ChronListingView();

        // Arrange.
        this.removeAll();
        this.add( view );
    }

    private void displayAuthenticatePanel ( Authenticator authenticator )
    {
        System.out.println( "BASIL - MainView::displayAuthenticatePanel. " + Instant.now() );
        // Widgets
        AuthenticateView authView = new AuthenticateView(this, authenticator);

        // Arrange.
//        this.getStyle().set( "border" , "6px dotted DarkOrange" );  // DEBUG - Visually display the  bounds of this layout.
        this.getStyle().set( "background-color" , "LightSteelBlue" );
        this.setSizeFull();
        this.setJustifyContentMode( FlexComponent.JustifyContentMode.CENTER ); // Put content in the middle horizontally.
        this.setDefaultHorizontalComponentAlignment( FlexComponent.Alignment.CENTER ); // Put content in the middle vertically.

        this.removeAll();
        this.add( authView );
    }

    // Implements AuthenticateView.AuthenticationPassedObserver
    @Override
    public void authenticationPassed ( )
    {
        System.out.println( "BASIL - MainView::authenticationPassed. " + Instant.now() );
        this.display();
    }
}

推荐阅读