Search code examples
vaadinvaadin8

Register session to User


I'm building a prototype using Vaadin8 starting from a single-module template. I'm trying to assign a unique UI instance (a session) to each authenticated user, so that each user is presented with a particular type of content according to their own settings within the app. Here's my configuration:

@WebListener
public class Market implements ServletContextListener {

    public static ArrayList<User>users;

    public void contextInitialized(ServletContextEvent sce) {   

    users=new ArrayList<User>();
    User hau=new User("hau");
    users.add(hau);
    User moc=new User("moc");
    users.add(moc);
    }

    public void contextDestroyed(ServletContextEvent sce){}
}

public class User {
    public String username;
    public user(String username){this.username=username;}
}


public class MyUI extends UI {
    User us3r;
    @Override
    protected void init(VaadinRequest vaadinRequest) {
      final VerticalLayout layout = new VerticalLayout();
      String username;
      if (this.us3r==null) {username="Guest";}else {username=us3r.username;}
      Label who=new Label(username);
      TextField userfield=new TextField();
      Button login=new Button("login");
      login.addClickListener(new ClickListener() {
        @Override
        public void buttonClick(ClickEvent event) {
            for (User user:Market.users) {
            if (userfield.getValue().equals(user.username)) {
                us3r=user;Page.getCurrent().reload();return;
                }
            }Notification.show("No user "+userfield.getValue());
        }
    });
      Button logout=new Button("logout");
      logout.addClickListener(new ClickListener() {
        public void buttonClick(ClickEvent event) {
            if(us3r!=null) {us3r=null; Page.getCurrent().reload();}
        }
    });
       layout.addComponent(userfield);
       layout.addComponent(login);
       layout.addComponent(who);
       layout.addComponent(logout);
       setContent(layout);
    }

After inputting one of the two usernames registered in the Database, I'd like the Label object to display the name of the authenticated user, instead of "Guest". Another effect I'm trying to achieve is if a user is logged in and there is another request to the server, it should generate a fresh UI with the uninstantiated us3r attribute.


Solution

  • Caveats: I have been using Vaadin Flow lately rather than Vaadin 8. So my memory is hazy, and my code may be wrong. And I have kept all the examples overly simple, not ready for production. Lastly, I am sure others would take a different approach, so you may want to do some internet searching to see alternatives.


    UI is malleable

    The UI of Vaadin is more plastic and malleable than you may realize. You can entirely replace the initial VerticalLayout with some other widget-containing-view.

    The way I have handled logins with Vaadin is that my default UI subclass checks for an object of my own User class in the web session. Being based on Jakarta Servlet technology, every Vaadin web app automatically benefits from the Servlet-based session handling provided by the Servlet container. Furthermore, Vaadin wraps those as a VaadinSession.

    If the User object is found to be existing as an "attribute" (key-value pair) in the session, then I know the user has already logged-in successfully. So I display the main content in that initial UI subclass object. By "main content", I mean an instance of a particular class I wrote that extends VertialLayout, or HoriontalLayout or some such.

    If no User object is found, then my initial UI subclass object displays a login view. By "login view" I mean an instance of some other particular class I wrote that extends VertialLayout, or HoriontalLayout or some such.

    When you switch or morph the content within a UI subclass instance, Vaadin takes care of all the updating of the client. The change in state of your UI object on the server made by your Java code is automatically communicated to the Vaadin JavaScript library that was initially installed in the web browser. That Vaadin JS library automatically renders your changed user-interface by generating the needed HTML, CSS, JavaScript, and so on. There is no need for you to be reloading the page as you seem to be doing in your example code. As a single-page web app, the web page only loads once. In Vaadin, we largely forget about the HTTP Request/Response cycle.

    Example app

    First we need a simple User class for demonstration purposes.

    package work.basil.example;
    
    import java.time.Instant;
    import java.util.Objects;
    
    public class User
    {
        private String name;
        private Instant whenAuthenticated;
    
        public User ( String name )
        {
            Objects.requireNonNull( name );
            if ( name.isEmpty() || name.isBlank() ) { throw new IllegalArgumentException( "The user name is empty or blank. Message # b2ec1529-47aa-47c1-9702-c2b2689753cd." ); }
            this.name = name;
            this.whenAuthenticated = Instant.now();
        }
    
        @Override
        public boolean equals ( Object o )
        {
            if ( this == o ) return true;
            if ( o == null || getClass() != o.getClass() ) return false;
            User user = ( User ) o;
            return name.equals( user.name );
        }
    
        @Override
        public int hashCode ( )
        {
            return Objects.hash( name );
        }
    }
    

    The starting point of our app, our subclass of UI checks the session and switches content. Notice how we segregated the check-and-switch code to a named method, ShowLoginOrContent. This allows us to invoke that code again after login, and again after logout.

    package work.basil.example;
    
    import com.vaadin.annotations.Theme;
    import com.vaadin.annotations.VaadinServletConfiguration;
    import com.vaadin.server.VaadinRequest;
    import com.vaadin.server.VaadinServlet;
    import com.vaadin.server.VaadinSession;
    import com.vaadin.ui.UI;
    
    import javax.servlet.annotation.WebServlet;
    import java.util.Objects;
    
    /**
     * This UI is the application entry point. A UI may either represent a browser window
     * (or tab) or some part of an HTML page where a Vaadin application is embedded.
     * <p>
     * The UI is initialized using {@link #init(VaadinRequest)}. This method is intended to be
     * overridden to add component to the user interface and initialize non-component functionality.
     */
    @Theme ( "mytheme" )
    public class MyUI extends UI
    {
    
        @Override
        protected void init ( VaadinRequest vaadinRequest )
        {
            this.showLoginOrContent();
        }
    
        void showLoginOrContent ( )
        {
            // Check for User object in session, indicating the user is currently logged-in.
            User user = VaadinSession.getCurrent().getAttribute( User.class );
            if ( Objects.isNull( user ) )
            {
                LoginView loginView = new LoginView();
                this.setContent( loginView );
            } else
            {
                CustomerListingView customerListingView = new CustomerListingView();
                this.setContent( customerListingView );
            }
        }
    
        @WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
        @VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )
        public static class MyUIServlet extends VaadinServlet
        {
        }
    }
    

    screenshot of login view with "Sign in" button

    Here is that LoginView, a VerticalLayout. We have our username & password, with a "Sign in" button. Notice how on successful authentication we:

    • Instantiate a User and add to the automatically-created session as an "attribute" key-value pair. The key is the class User, and the value is the User instance. Alternatively, you can choose to use a String as the key.
    • Invoke that showLoginOrContent method on MyUI to swap out our login view with a main content view.

    In real work, I would locate the user-authentication mechanism to its own class unrelated to the user-interface. But here we ignore the process of authentication for this demonstration.

    package work.basil.example;
    
    import com.vaadin.server.VaadinSession;
    import com.vaadin.ui.*;
    
    public class LoginView extends VerticalLayout
    {
        private TextField userNameField;
        private PasswordField passwordField;
        private Button authenticateButton;
    
        public LoginView ( )
        {
            // Widgets
            this.userNameField = new TextField();
            this.userNameField.setCaption( "User-account name:" );
    
            this.passwordField = new PasswordField();
            this.passwordField.setCaption( "Passphrase:" );
            this.authenticateButton = new Button( "Sign in" );
            this.authenticateButton.addClickListener( ( Button.ClickListener ) clickEvent -> {
                        // Verify user inputs, not null, not empty, not blank.
                        // Do the work to authenticate the user.
                        User user = new User( this.userNameField.getValue() );
                        VaadinSession.getCurrent().setAttribute( User.class , user );
                        ( ( MyUI ) UI.getCurrent() ).showLoginOrContent(); // Switch out the content in our `UI` subclass instance.
                    }
            );
    
            // Arrange
            this.addComponents( this.userNameField , this.passwordField , this.authenticateButton );
        }
    }
    

    main content view, with "Sign out" button

    Lastly, we need our main content view. Here we use a "customer listing" that is not yet actually built. Instead, we place a couple pieces of text so you know the layout is appearing. Notice how in this code we look up the user's name from our User object in the session attribute.

    We include a "Sign out" button to show how we reverse the authentication simply by clearing our User instance as the value of our "attribute" on the session. Alternatively, you could kill the entire session by calling VaadinSession::close. Which is appropriate depends on your specific app.

    package work.basil.example;
    
    import com.vaadin.server.VaadinSession;
    import com.vaadin.ui.Button;
    import com.vaadin.ui.Label;
    import com.vaadin.ui.UI;
    import com.vaadin.ui.VerticalLayout;
    
    import java.time.Duration;
    import java.time.Instant;
    
    public class CustomerListingView extends VerticalLayout
    {
        Button logoutButton;
    
        public CustomerListingView ( )
        {
            // Widgets
            this.logoutButton = new Button( "Sign out" );
            this.logoutButton.addClickListener( ( Button.ClickListener ) clickEvent -> {
                        VaadinSession.getCurrent().setAttribute( User.class , null ); // Pass null to clear the value.
                        ( ( MyUI ) UI.getCurrent() ).showLoginOrContent();
                    }
            );
            User user = VaadinSession.getCurrent().getAttribute( User.class );
            Duration duration = Duration.between( user.getWhenAuthenticated() , Instant.now() );
            Label welcome = new Label( "Bonjour, " + user.getName() + ". You’ve been signed in for: " + duration.toString() + "." );
            Label placeholder = new Label( "This view is under construction. A table of customers will appear here.\"" );
    
            // Arrange
            this.addComponents( this.logoutButton , welcome , placeholder );
        }
    }
    

    The effect of the "Sign out" button is to remove the main content, and take the user back to the login view.

    Separation of concerns

    One of the aims of the approach to logins is separation of concerns. The concern of building an interactive user-interface (Vaadin widgets and code) should be kept largely separate from the business logic of how we determine if a user is who they claim to be (authentication code).

    Our UI subclass knows almost nothing about user-authentication. We moved all the mechanics of logging-in to other non-Vaadin-specific classes. The Vaadin-related code only has two connection points to authentication: (a) Passing collected credentials (username, password, or such), and (b) Checking for the presence of a User object in the session’s key-value store.

    Multi-window web apps

    By the way, you should know that Vaadin 8 has amazing support for multi-window web apps. You can write links or buttons to open additional windows/tabs in the browser, all working within the same web app and the same user session. Each tab/window has its own instance of a UI subclass you wrote. All of these UI subclass instances share the same VaadinSession object.

    So using the logic seen above applies to all such tab/windows: Multiple windows all belonging to one session with one login.

    Fake dialog boxes are not secure

    You might be tempted to put your login view inside a dialog box appearing over your main content. Do not do this. A web dialog box is “fake”, in that it is not a window created and operated by the operating-system. A web app dialog window is just some graphics to create the illusion of a second window. The pretend dialog and the underlying content are actually all one web page.

    A hacker might gain access to the content on the page, and might be able to defeat your login dialog. This is mentioned in the Vaadin manual, on the page Sub-Windows.

    In my example above, we have no such security problem. The sensitive main content arrives on the user’s web browser only after authentication completes.

    Web app lifecycle hooks

    By the way, your use of ServletContextListener is correct. That is the standard hook for the lifecycle of your web app launching. That listener is guaranteed to run before the first user’s request arrives, and again after the last user’s response is sent. This is an appropriate place to configure resources needed by your app in general, across various users.

    However, in Vaadin, you have an alternative. Vaadin provides the VaadinServiceInitListener for you to implement. This may be more convenient than the standard approach, though you need to configure it by creating a file to make your implementation available via the Java Service Implementation Interface (SPI) facility. Your VaadinServiceInitListener as another place to setup resources for your entire web app. You can also register further listeners for the service (web app) shutting down, and for user-session starting or stopping.

    Browser Reload button

    One last tip: You may want to use the @PreserveOnRefresh annotation.

    Vaadin Flow

    In Vaadin Flow (versions 10+), I take the same approach to logins.

    Well, basically the same. In Vaadin Flow, the purpose of the UI class was dramatically revamped. Actually, that class should have been renamed given how differently it behaves. We no longer routinely write a subclass of UI when starting a Vaadin app. A UI instance is no longer stable during the user's session. The Vaadin runtime will replace the UI object by another new instance (or re-initialize it), sometimes quite rapidly, for reasons I do not yet understand. So I do not see much practical use for UI for those of us writing Vaadin apps.

    Now in Flow I start with an empty layout, instead of a UI subclass. Inside that layout I swap nested layouts. First the login view. After authentication, I swap the login view for the main content view. On logout, the opposite (or close the VaadinSession object).