我知道在使用Vaadin 14的Login
组件时,我必须调用addLoginListener
来注册我自己的实现ComponentEventListener的侦听器
我的侦听器代码通过什么机制将身份验证检查的结果传达回我的LoginForm
?
如果身份验证检查成功,我需要我的LoginForm
关闭并让导航继续到预期的路线。如果身份验证失败,我需要LoginForm
通知用户失败,并要求用户重新输入用户名和密码。我的侦听器代码如何告诉LoginForm下一步要做什么?
LoginForm
可以为两种事件注册侦听器:
这两种方法对我们检测用户是否成功完成登录尝试都没有用。无论注册“登录”按钮单击的方法是什么,都将决定用户的成功或失败。正是这种方法需要发出登录尝试成功的信号。
作为组件
,LoginForm
内置支持为其他类型的事件注册侦听器。不幸的是,这种支持的范围仅限于protected
。因此事件侦听器支持不能扩展到我们的Web应用程序类,因为它们不是Vaadin包的一部分(超出范围)。
我尝试使用Vaadin Flow的路由功能,但失败了。Vaadin 14.0.2中的路由行为似乎存在一些严重问题。加上我对路由的无知,这对我来说是一个禁忌。相反,我使用单个URL(root"")从我的MainView
中管理所有内容。
我不知道这是否是最好的方法,但我选择制作一个名为AuthenticateView
的LoginForm
子类。
在那个子类上,我添加了一个嵌套的接口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
子类,并安装当前用户有权查看的常规应用内容。
另请注意注释:
@P要在用户单击浏览器刷新按钮的情况下使内容保持活动状态。这可能有助于登录过程,我想我还没有想清楚。
@PWA
。我目前不需要渐进式Web应用程序功能。但是在Vaadin 14.0.2中删除该注释似乎会导致包含我们的Web浏览器窗口/选项卡内容的UI
对象的更多虚假替换,所以我保留了这一行。@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
配置中设置.成功处理程序
属性。
现在您应该能够拦截成功登录,在我的例子中,登录登录用户。
对于失败的登录尝试,情况也是如此。
希望这有帮助。