vaadin
有些事情最好保密。 这也适用于数据。 在本教程中,您将学习如何使用普通的Vaadin和Java实现简单的访问控制系统。
我们分四个步骤进行操作:创建登录视图,授权用户,对用户进行身份验证以及创建注销按钮。
本教程既不介绍如何存储用户凭据,也不介绍如何保护静态资源(如CSS和图像)。
此处使用的方法可以应用于大多数Vaadin项目,但是它们是为vaadin.com/start上的Plain Java Servlet入门编写的 ,在撰写本文时使用的是Vaadin版本14.1.5
。
下载并解压缩项目后,可以使用mvn jetty:run
从命令行运行它。 然后可以在localhost:8080
访问该应用程序和MainView
。
创建登录屏幕
登录需要登录屏幕。 为了简单起见,我们使用Vaadin的LoginForm 。
让我们创建一个类LoginView
,它扩展了VerticalLayout
,并具有一个包含LoginForm
的私有字段。
public class LoginView extends VerticalLayout {
private LoginForm loginForm ;
}
最好推迟组件初始化直到附加组件。 这样可以避免为永远不会显示给用户的组件运行潜在的昂贵代码。 由于Flow错误#4595 ,即使通过BeforeEnterEvent
事件限制了对组件的访问,构造函数也会运行。
我们可以通过重写onAttach
方法来进行初始化。 要仅运行一次,我们使用AttachEvent#isInitialAttach
方法检查这是否是第一个附加事件。
LoginView.java
@Override
public void onAttach ( AttachEvent event ) {
if ( event . isInitialAttach ()) {
initialize ();
}
}
然后,我们可以创建初始化方法,在该方法中实例化登录表单。 我们将表格在水平和垂直方向上居中对齐,然后隐藏“ Forgot password?
选项,然后再将其添加到布局中。
LoginView.java
private void initialize () {
setAlignItems ( Alignment . CENTER );
setJustifyContentMode ( JustifyContentMode . CENTER );
setSizeFull ();
loginForm = new LoginForm ();
loginForm . setForgotPasswordButtonVisible ( false );
add ( loginForm );
}
要启用导航到视图,我们使用@Route
对其进行@Route
。 如果未指定任何路由,则该路由将是没有“ view”的类名,在这种情况下为login
。
通过实现ComponentEventListener<AbstractLogin.LoginEvent>
可以在此视图中处理登录事件,在初始化方法中,可以使用loginForm.addLoginListener(this);
将视图添加为侦听器,从而处理登录事件loginForm.addLoginListener(this);
。
目前,组件事件侦听器方法只能在表单上设置错误,该表单显示一条消息,提示用户名或密码不正确。
LoginView.java
@Override
public void onComponentEvent ( AbstractLogin . LoginEvent loginEvent ) {
loginForm . setError ( true );
}
重建或重新启动应用程序之后,我们现在可以导航至http://localhost:8080/login
,并且看到无论输入用户名和密码,我们始终会收到错误消息。
授权用户
授权是确定用户是否可以执行特定操作的过程。 在我们的情况下,如果用户经过身份验证,则有权访问该视图。 授权在进入任何视图之前完成。
让我们从创建一个SecurityService
类开始,该类负责检查用户是否已通过身份验证。 通过创建一个私有构造函数,并在getInstance()
方法中返回一个静态实例,使它成为一个单例类。
可以
synchronized
getInstance()
方法以防止由于并发访问而创建多个实例。 在这种情况下,由于没有状态存储在类中,因此我们忽略了它以提高性能。
public class SecurityService {
private static SecurityService instance ;
private SecurityService () {}
public static SecurityService getInstance () {
if ( instance == null ) {
instance = new SecurityService ();
}
return instance ;
}
}
当用户登录时,我们将用户名作为会话属性存储在任意键下。 这样,我们可以通过检查该属性是否设置来检查用户是否登录。
SecurityService.java
// This can be anything, but we want to avoid accidentally using the same name for different purposes
private static final String USER_ATTRIBUTE = "SecurityService.User" ;
...
public boolean isAuthenticated () {
return VaadinSession . getCurrent () != null &&
VaadinSession . getCurrent (). getAttribute ( USER_ATTRIBUTE ) != null ;
}
接下来,我们需要在进入任何视图之前调用此方法。 为此,我们利用了添加到每个UI
的BeforeEnterListener
。 每当创建UI
时,我们都可以使用VaadinServiceInitListener进行添加。
我们创建一个实现VaadinServiceInitListener
接口的类,并实现serviceInit
方法。
public class MyServiceInitListener implements VaadinServiceInitListener {
@Override
public void serviceInit ( ServiceInitEvent serviceInitEvent ) {
}
}
为了在启动时进行注册,我们必须创建src/main/resources/META-INF/services/
目录并添加一个名为com.vaadin.flow.server.VaadinServiceInitListener
的文件。 文件中唯一的内容应该是我们的服务初始化监听器的全限定名称,在本例中为org.vaadin.erik.MyServiceInitListener
。
现在我们已经着手了服务初始化,我们可以使用初始化事件来监听UI
初始化。 接下来,我们将类更改为也实现UIInitListener
,并通过ServiceInitEvent
对其进行注册。
MyServiceInitListener.java
@Override
public void serviceInit ( ServiceInitEvent serviceInitEvent ) {
serviceInitEvent . getSource (). addUIInitListener ( this );
}
现在,通过实现uiInit
方法将类作为BeforeEnterListener
添加到新创建的UI
我们可以进一步深入研究。 为此,我们还需要实现BeforeEnterListener
。
MyServiceInitListener.java
@Override
public void uiInit ( UIInitEvent uiInitEvent ) {
uiInitEvent . getUI (). addBeforeEnterListener ( this );
}
最后,我们实现beforeEnter
方法来检查用户是否已通过身份验证,如果未通过身份验证,则将用户转发到登录视图。
MyServiceInitListener.java
@Override
public void beforeEnter ( BeforeEnterEvent beforeEnterEvent ) {
boolean authenticated = SecurityService . getInstance (). isAuthenticated ();
if ( beforeEnterEvent . getNavigationTarget (). equals ( LoginView . class )) {
if ( authenticated ) {
beforeEnterEvent . forwardTo ( MainView . class );
}
return ;
}
if (! authenticated ) {
beforeEnterEvent . forwardTo ( LoginView . class );
}
}
导航到LoginView
,代码包含一种特殊情况,以避免因一次又一次触发beforeEnter
方法而陷入将用户再次转发到LoginView
的循环中。 相反,如果用户已登录,则将其转发到MainView
。 否则不采取任何措施。
现在运行该应用程序,您将看到不再可以访问MainView
,并且导航到http://localhost:8080
会将您转发到登录视图。
验证用户
现在,我们可以通过确定提供的用户名和密码是否正确来认证用户。
我们在SecurityService
添加了一个名为authenticate
的方法,该方法以用户名和密码作为参数,如果身份验证成功,则返回true
。 在这种情况下,它还将用户名存储在会话中。
本教程没有详细介绍身份验证过程,而是将用户名和密码直接存储在类文件中。 这意味着有权访问源代码的任何人都可以看到凭据。 至少应使用Bcrypt
类的哈希函数对密码进行哈希Bcrypt
。
SecurityService.java
private static final String USERNAME = "admin" ;
private static final String PASSWORD = "password" ;
public boolean authenticate ( String username , String password ) {
if ( USERNAME . equals ( username ) && PASSWORD . equals ( password )) {
VaadinSession . getCurrent (). setAttribute ( USER_ATTRIBUTE , username );
return true ;
}
return false ;
}
在登录视图中,我们调用此方法,并且仅在该方法返回false时设置错误。 否则,我们会将用户重定向到MainView
。 现在我们已经在项目中两次对值MainView.class
进行了硬编码,因此我们应该在安全服务中添加一个返回默认视图的方法,并更改以前的用法以使用此新方法。
LoginView.java
@Override
public void onComponentEvent ( AbstractLogin . LoginEvent loginEvent ) {
boolean success = SecurityService . getInstance ()
. authenticate ( loginEvent . getUsername (), loginEvent . getPassword ());
if ( success ) {
UI . getCurrent (). navigate ( SecurityService . getInstance (). getDefaultView ());
} else {
loginForm . setError ( true );
}
}
SecurityService.java
public Class <? extends Component > getDefaultView () {
return MainView . class ;
}
此时,我们可以使用凭据admin
/ password
登录到应用程序。 但是,我们还不能注销。
注销
为了能够注销,我们需要SecurityService
的方法来注销。 此方法有两件事:
- 它将使包装
VaadinSession
HttpSession
无效,关闭其包含的所有VaadinSession
实例。 这将导致在下一次导航时创建新的VaadinSession
,从而有效清除存储的用户。 - 它将用户重定向到登录视图。
SecurityService.java
public void logOut () {
VaadinSession . getCurrent (). getSession (). invalidate ();
UI . getCurrent (). navigate ( LoginView . class );
}
WAR文件中的所有servlet共享相同的
HttpSession
,但具有自己的VaadinSession
实例。 如果您只想关闭特定的Vaadin会话,请调用VaadinSession.getCurrent().close()
。
现在我们需要从某个地方调用此方法。 我们添加了一个用于注销到MainView
的按钮,同时将所有初始化都移到onAttach
,就像在登录视图中一样。 我们还将从主视图中删除默认组件。
MainView.java
private void initialize () {
Button logOutButton = new Button ( "Log out" );
logOutButton . getStyle (). set ( "margin-left" , "auto" );
logOutButton . addClickListener ( e -> SecurityService . getInstance (). logOut ());
HorizontalLayout toolbar = new HorizontalLayout ( logOutButton );
toolbar . setWidthFull ();
add ( toolbar );
add ( new Span ( "Hello, you are logged in" ));
}
public void onAttach ( AttachEvent event ) {
if ( event . isInitialAttach ()) {
initialize ();
}
}
该按钮仅调用logOut
方法。 我们将左边距设置为auto
,以使按钮向右浮动。
现在,当我们测试应用程序时,我们首先无法访问MainView
。 登录后,但仅使用正确的凭据,我们才被转发到主视图。 现在导航到登录视图使我们回到主视图。
单击注销按钮后,除非再次登录,否则我们将无法再访问主视图。
恭喜,您的应用程序现在已设置访问控制!
完整的代码可以在GitHub上找到 。
封面图片由dying_grotesque根据CC BY 2.0许可
翻译自: https://dev.to/eriklumme/simple-access-control-in-vaadin-53d0
vaadin