Ein Blog

Gesammelte Notizen

Spring Boot and Vaadin with Maven - Security

13 Januar, 2016 | Java

Now we need some security. Required are different users with different roles. If the user is not authorized he only the main page and the login page should be visible. There there should be a normal user that should be able to acces all pages except the sensor configuration page. And there is the admin user with access to all pages.

Because Spring Boot is used the first idea was to use Spring Security.

There are some examples how to use it at stackoverflow: integrate-spring-boot-starter-security-with-vaadin-7 or you can use the vaadin4spring addons.

In my opinion Spring Security do not fit together, you have to add to much exceptions to get Vaadin running.

Therefore I had a look at the other big player Apache Shiro.

And it matched exactly my needs. It's easy to use and has user and roles. So I added it to the Webfrontend.

The app uses an ini file to store the users and the roles, the file is called shiro.ini and is stored in src/main/resources and looks like this

# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, user
sensor = huhu, sensor

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
user = user:*
sensor = sensor:view

Now we have to add the security handling to the application:

First add a security factory to the application class VaadinSensorFrontenendApplication 

package de.jefure.sensorClient;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class VaadinSensorFrontenendApplication {
    
    public static void main(String[] args) {
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityUtils.setSecurityManager(factory.createInstance());
        SpringApplication.run(VaadinSensorFrontenendApplication.class, args);
    }
}

And then implement the permission handling to the UI class where the navigation is handled. Here it is the class SensorFrontendUI 

package de.jefure.sensorClient;

import com.vaadin.annotations.Theme;
import com.vaadin.annotations.Widgetset;
import com.vaadin.navigator.Navigator;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
import com.vaadin.server.VaadinRequest;
import com.vaadin.spring.annotation.SpringUI;
import com.vaadin.spring.navigator.SpringViewProvider;
import com.vaadin.ui.Button;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.Panel;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;
import de.jefure.sensorClient.view.DefaultView;
import de.jefure.sensorClient.view.LoginView;
import de.jefure.sensorClient.view.SensorDataView;
import de.jefure.sensorClient.view.SensorView;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

@Theme("valo")
@SpringUI
@Widgetset("de.jefure.sensorClient.AppWidgetSet")
public class SensorFrontendUI extends UI {

    @Autowired
    private SpringViewProvider viewProvider;

    private final Button loginBtn;
    private final Button defaultBtn;
    private final Button sensorBtn;
    private final Button dataBtn;
    private final Button logoutBtn;

    public SensorFrontendUI() {
        loginBtn = createNavigationButton("Login", LoginView.NAME);
        defaultBtn = createNavigationButton("Start", DefaultView.NAME);
        sensorBtn = createNavigationButton("Sensors", SensorView.NAME);
        dataBtn = createNavigationButton("Data", SensorDataView.NAME);
        logoutBtn = createLogoutButton();
        setButtonDefault();
    }

    @Override
    protected void init(VaadinRequest request) {
        VerticalLayout vl = new VerticalLayout();
        vl.setMargin(true);
        vl.setSizeFull();
        vl.setSpacing(true);

        final CssLayout navLayout = new CssLayout();
        navLayout.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
        navLayout.addComponent(loginBtn);
        navLayout.addComponent(defaultBtn);
        navLayout.addComponent(sensorBtn);
        navLayout.addComponent(dataBtn);
        navLayout.addComponent(logoutBtn);

        vl.addComponent(navLayout);

        final Panel viewContainer = new Panel();
        viewContainer.setSizeFull();
        vl.addComponent(viewContainer);
        vl.setExpandRatio(viewContainer, 1.0f);

        setContent(vl);

        Navigator navigator = new Navigator(this, viewContainer);
        navigator.addProvider(viewProvider);

        getNavigator().addViewChangeListener(new ViewChangeListener() {

            @Override
            public boolean beforeViewChange(ViewChangeEvent event) {
		// get the user with it's credentials
                Subject currentUser = (Subject) getUI().getSession().getAttribute("user");
		// and check if the user is authencated.
                boolean isLoggedIn = currentUser == null ? false : currentUser.isAuthenticated();
                boolean isLoginView = event.getNewView() instanceof LoginView;
                boolean isDefaultView = event.getNewView() instanceof DefaultView;

                if (!isLoggedIn && isDefaultView) {                 
                    return true;
                } else if (!isLoggedIn && !isLoginView) {
                    // Redirect to login view always if a user has not yet
                    // logged in
                    getNavigator().navigateTo(LoginView.NAME);
                    return false;
                } else if (isLoggedIn && isLoginView) {
                    // If someone tries to access to login view while logged in,
                    // then cancel
                    return false;
                }

                return true;
            }

            @Override
            public void afterViewChange(ViewChangeEvent event) {
		// enable or disable the nav buttons depending on user role
                Subject currentUser = (Subject) getUI().getSession().getAttribute("user");
                if (currentUser != null && currentUser.isAuthenticated()) {
                    if (currentUser.hasRole("admin")) {
                        sensorBtn.setEnabled(true);
                        dataBtn.setEnabled(true);
                    } else if (currentUser.hasRole("user")) {
                        sensorBtn.setEnabled(false);
                        dataBtn.setEnabled(true);
                    }
                    defaultBtn.setEnabled(true);
                    logoutBtn.setVisible(true);
                    loginBtn.setVisible(false);
                } else {
                    setButtonDefault();
                }
            }
        });
    }

    private Button createNavigationButton(String caption, final String viewName) {
        Button button = new Button(caption);
        button.addStyleName(ValoTheme.BUTTON_SMALL);
        button.addClickListener(event -> getUI().getNavigator().navigateTo(viewName));
        return button;
    }

    private Button createLogoutButton() {
        Button button = new Button("Logout");
        button.addStyleName(ValoTheme.BUTTON_SMALL);
        button.addClickListener(event -> {
            getUI().getSession().setAttribute("user", null);
            getUI().getNavigator().navigateTo(DefaultView.NAME);
        });
        return button;
    }

    private void setButtonDefault() {
        defaultBtn.setEnabled(true);
        sensorBtn.setEnabled(false);
        dataBtn.setEnabled(false);
        logoutBtn.setVisible(false);
        loginBtn.setVisible(true);
    }
}

An the we need a login form, there fore a login view is added

package de.jefure.sensorClient.view;

import javax.annotation.PostConstruct;
import com.vaadin.navigator.View;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.shared.ui.MarginInfo;
import com.vaadin.spring.annotation.SpringView;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.PasswordField;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.jboss.logging.Logger;


@SpringView(name = LoginView.NAME)
public class LoginView extends CustomComponent implements View {

    public static final String NAME = "login";

    private TextField userField;

    @PostConstruct
    void init() {
        setSizeFull();

        userField = new TextField("Username:");
        userField.setWidth(300f, Unit.PIXELS);
        userField.setRequired(true);
        userField.setRequiredError("User name is required.");

        PasswordField passwordField = new PasswordField("Passwort");
        passwordField.setWidth(300f, Unit.PIXELS);
        passwordField.setRequired(true);
        passwordField.setRequiredError("Passwort is required.");

        Button loginButton = new Button("Login");
        loginButton.setWidth(300f, Unit.PIXELS);

        loginButton.addClickListener(event -> login(userField.getValue(), passwordField.getValue()));

        // Add both to a panel
        VerticalLayout fields = new VerticalLayout(userField, passwordField, loginButton);
        fields.setCaption("Please login to access the application. (guest/guest)");
        fields.setSpacing(true);
        fields.setMargin(new MarginInfo(true, true, true, false));
        fields.setSizeUndefined();

        // The view root layout
        VerticalLayout viewLayout = new VerticalLayout(fields);
        viewLayout.setSizeFull();
        viewLayout.setComponentAlignment(fields, Alignment.MIDDLE_CENTER);
        setCompositionRoot(viewLayout);
    }

    @Override
    public void enter(ViewChangeListener.ViewChangeEvent event) {
        userField.focus();
    }

    private void login(String userName, String pwd) {
        Subject currentUser = SecurityUtils.getSubject();
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(userName, pwd);
            token.setRememberMe(true);
            try {
                currentUser.login(token);
                getUI().getSession().setAttribute("user", currentUser);
                getUI().getNavigator().navigateTo(DefaultView.NAME);
            } catch (UnknownAccountException uae) {
                Logger.getLogger(getClass().getName()).warn("User unknown", uae);
            } catch (IncorrectCredentialsException ice) {
                Logger.getLogger(getClass().getName()).warn("Incorrect credentials", ice);
            } catch (LockedAccountException lae) {
                Logger.getLogger(getClass().getName()).warn("Account locked", lae);
            } catch (AuthenticationException ae) {
                Logger.getLogger(getClass().getName()).warn("Error auth", ae);
            }
        }
    }
}