authentication - 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?
解决方案
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
子类的控制器
最后,这是我早期尝试修改MainView
Vaadin Starter 项目给我的类的实验。
注意如何MainView
实现嵌套回调接口AuthenticateView.AuthenticationPassedObserver
。当用户成功完成登录后,将调用authenticationPassed
此处的方法。该方法从 的内容显示中MainView
清除子类,并安装当前用户有权查看的常规应用内容。LoginForm
MainView
还要注意注释:
- 尽管用户单击浏览器刷新按钮,但版本 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();
}
}
推荐阅读
- javascript - 如何将动态 Javascript div 高度调整与页内锚超链接结合起来?
- c++ - 用提升精神解析固定宽度的数字
- mongodb - 如何使用 pymongo 在 mongodb 中批量插入文档,同时跳过重复项
- reactjs - Firebase 使用 github 操作成功部署了 react 应用程序,但尽管在 firebase.json 中设置了“public”:“build”,但仍会导致空白屏幕
- javascript - HTML 输入在移动设备上通过内容进行裁剪
- php - mysql循环问题
- php - 使用 JS 将内容放在 AFTER 之后的 PHP 抓取网站
- python - 将 Python Discord Bot 发送给 DM 硬编码接收者加入的人的姓名
- python - 将条件应用于 Odoo 14 中的 OOTB“编辑”按钮
- java - Mac 上的 Eclipse 不允许我选择文件