提问者:小点点

Vaadin LoginForm-用户通过或认证失败时的信令


我知道在使用Vaadin 14的Login组件时,我必须调用addLoginListener来注册我自己的实现ComponentEventListener的侦听器

我的侦听器代码通过什么机制将身份验证检查的结果传达回我的LoginForm

如果身份验证检查成功,我需要我的LoginForm关闭并让导航继续到预期的路线。如果身份验证失败,我需要LoginForm通知用户失败,并要求用户重新输入用户名和密码。我的侦听器代码如何告诉LoginForm下一步要做什么?


共2个答案

匿名用户

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

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

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

作为组件LoginForm内置支持为其他类型的事件注册侦听器。不幸的是,这种支持的范围仅限于protected。因此事件侦听器支持不能扩展到我们的Web应用程序类,因为它们不是Vaadin包的一部分(超出范围)。

我尝试使用Vaadin Flow的路由功能,但失败了。Vaadin 14.0.2中的路由行为似乎存在一些严重问题。加上我对路由的无知,这对我来说是一个禁忌。相反,我使用单个URL(root"")从我的MainView中管理所有内容。

我不知道这是否是最好的方法,但我选择制作一个名为AuthenticateViewLoginForm子类。

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

请注意,我们不关心失败。我们只需将LoginForm子类作为我们的MainView内容显示,直到用户成功或关闭Web浏览器窗口/选项卡,从而终止会话。如果您担心黑客无休止地尝试登录,您可能希望您的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。但现在已经足够好了。

我写了一些实现。有几个是用于初始设计的:一个总是不经检查就接受凭据,另一个总是不经检查就拒绝凭据。另一个实现是真正的,在用户数据库中查找。

身份验证器返回可选

这里总是-不及格:

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子类的更大上下文可能会改变。所以我不想硬编码对产生这个LoginForm子类对象的MainView对象的方法调用。
  • 我更喜欢更简单的方法,将方法引用传递给我的LoginForm子类的构造函数。然后我可以省略整个定义-nested-接口的事情。类似于这样:AuthenticateView AuthView=new AuthenticateView(这::authenticationPassed,验证器);。但是我对lambda不够精通,不知道如何定义我自己的方法引用参数。

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

请注意MainView如何实现嵌套回调接口AuthenticateView. AuthenticationPassed观察者。当用户成功完成登录时,MainView上的身份验证通过方法被调用。该方法从MainView的内容显示中清除LoginForm子类,并安装当前用户有权查看的常规应用内容。

另请注意注释:

  • Vaadin Flow在版本14中新增的@P要在用户单击浏览器刷新按钮的情况下使内容保持活动状态。这可能有助于登录过程,我想我还没有想清楚。
  • 另外,请注意@PWA。我目前不需要渐进式Web应用程序功能。但是在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();
    }
}

匿名用户

我知道这个问题已经得到了回答,但是我在Vaadin 14.8.1中遇到了一个类似的问题,没有找到答案。所以我想我不妨在这里描述一下它的解决方案。

在我的例子中,我想拦截成功登录以进行监控。基本上,我正在寻找一旦用户登录并记录用户名就会触发的事件。

如上一个答案所述,Vaadin的LoginForm没有为此提供任何回调。由于Vaadin 14使用Spring Boot,因此使用Spring Security,您可以使用它的功能来实现成功登录的侦听器。

要做到这一点,您需要创建一个自定义身份验证成功处理程序:

public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(LoginSuccessHandler.class);
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //Redirect to root page after login
        httpServletRequest.getRequestDispatcher("/").forward(httpServletRequest, httpServletResponse);
        LOGGER.info("User successfully logged in: {}", authentication.getName());
    }
}

这里需要覆盖onAuthentication成功方法。在Authentication对象中,您可以找到登录用户的用户名、密码和权限(角色)。

需要注意的是,如果您不重定向请求,您将被困在登录屏幕中。由于用户已登录并创建了会话,您可以重定向到每个路由,但我建议使用根路由。

现在您需要将处理程序添加到SecurityConfig:

@Override
  protected void configure(HttpSecurity http) throws Exception {
    // Vaadin handles CSRF internally
    http.csrf().disable()

        // Register our CustomRequestCache, which saves unauthorized access attempts, so the user is redirected after login.
        .requestCache().requestCache(new CustomRequestCache())

        // Restrict access to our application.
        .and().authorizeRequests()

        // Allow all Vaadin internal requests.
        .requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()

        // Allow all requests by logged-in users.
        .anyRequest().authenticated()

        // Configure the login page.
        .and().formLogin()
            .successHandler(new LoginSuccessHandler())
            .loginPage(LOGIN_URL).permitAll()
            .loginProcessingUrl(LOGIN_PROCESSING_URL)
            .failureUrl(LOGIN_FAILURE_URL)
        // Configure logout
        .and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);
  }

这里您只需要从. formLogin配置中设置.成功处理程序属性。

现在您应该能够拦截成功登录,在我的例子中,登录登录用户。

对于失败的登录尝试,情况也是如此。

希望这有帮助。