Build Secure Web Apps with Vaadin & Spring Boot
Guest post by Daniel Hügelmann
Dependable security with Spring Security
Almost all web application frameworks include their security-relevant functions and provide the respective interfaces to external security solutions. As a Java-based full-stack framework, Vaadin is compatible with the most popular Java security solutions. Worth naming here are Spring Security, Apache Shiro, and JAAS.
If you were using Vaadin and Spring boot to build web applications, then the Spring Framework’s Spring Security module would be the tool of choice. It is easily connected using Vaadin Flow’s Security Helpers. These allow view-based access control without the need of having to configure Spring Security greatly.
This allows each view to be secured separately with its security guidelines. The @DenyAll, @PermitAll, @RolesAllowed, and @AnonymousAllowed annotations are applied to the respective view classes.
For this, we require the following artifacts in the project: A log-in view, a mechanism for the user to log out, the Spring Security dependencies, a class for the security configuration (which extends VaadinWebSecurity), and one of the following annotations on each of our view classes: @PermitAll, @RolesAllowed, @AnonymousAllowed.
Naturally with a log-in view
The log-in view is our web application’s first bastion. This is where our users first authenticate themselves. Here we can use Vaadin’s log-in form component or implement our own log-in view. One can also work with modern authentication methods, such as the two-factor authentication offered by Google’s Authenticator Library. The following listing shows how a simple log-in view can be implemented with Vaadin’s component and i18n.
@Route @PageTitle("JTAF - Login") public class LoginView extends LoginOverlay implements AfterNavigationObserver, BeforeEnterObserver { @Serial private static final long serialVersionUID = 1L; public LoginView() { var i18n = LoginI18n.createDefault(); i18n.setHeader(new LoginI18n.Header()); i18n.getHeader().setTitle("JTAF"); i18n.getHeader().setDescription("Track and Field"); i18n.setAdditionalInformation(null); i18n.setForm(new LoginI18n.Form()); i18n.getForm().setSubmit(getTranslation("Sign.in")); i18n.getForm().setTitle(getTranslation("Sign.in")); i18n.getForm().setUsername(getTranslation("Email")); i18n.getForm().setPassword(getTranslation("Password")); i18n.getErrorMessage().setTitle(getTranslation("Auth.ErrorTitle")); i18n.getErrorMessage().setMessage(getTranslation("Auth.ErrorMessage")); setI18n(i18n); setForgotPasswordButtonVisible(false); setAction("login"); UI.getCurrent().getPage().executeJs("document.getElementById('vaadinLoginUsername').focus();"); } @Override public void beforeEnter(BeforeEnterEvent event) { if (SecurityContext.isUserLoggedIn()) { event.forwardTo(DashboardView.class); } else { setOpened(true); } } @Override public void afterNavigation(AfterNavigationEvent event) { setError(event.getLocation().getQueryParameters().getParameters().containsKey("error")); } }
Spring Boot security dependencies
To use the Spring Security Module, we have to add the Spring Boot Starter security dependency to our project. For this we use the groupID org.springframework.boot and the artifactID spring-boot-starter-security:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
Log-out
Our user should not just be logged out via time-out; he should also be able to log-out manually. For this, we can create a simple log-out button in our Header:
public class MainLayout extends AppLayout { private SecurityService securityService; public MainLayout(@Autowired SecurityService securityService) { this.securityService = securityService; H1 logo = new H1("Unsere Web Applikation"); logo.addClassName("logo"); HorizontalLayout header; if (securityService.getAuthenticatedUser() != null) { Button logout = new Button("Log-Out", click -> securityService.logout()); header = new HorizontalLayout(logo, logout); } else { header = new HorizontalLayout(logo); } addToNavbar(header); } }
This log-out button is then equipped with the Spring Security API’s functionality by implementing a SecurityService class:
@Component public class SecurityService { private static final String LOGOUT_SUCCESS_URL = "/"; public UserDetails getAuthenticatedUser() { SecurityContext context = SecurityContextHolder.getContext(); Object principal = context.getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { return (UserDetails) context.getAuthentication().getPrincipal(); } // Anonymous or no authentication. return null; } public void logout() { UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL); SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); logoutHandler.logout( VaadinServletRequest.getCurrent().getHttpServletRequest(), null, null); }
The Security Configuration
We now need to extend the VaadinWebSecurity class. For this, we create the class SecurityConfiguration:
@EnableWebSecurity @Configuration public class SecurityConfiguration extends VaadinWebSecurity { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/public/**") .permitAll(); super.configure(http); setLoginView(http, LoginView.class); } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Bean public UserDetailsManager userDetailsService() { UserDetails user = User.withUsername("NormalUser") .password("{noop}user") .roles("STANDARDROLE") .build(); UserDetails admin = User.withUsername("Administrator") .password("{noop}admin") .roles("ADMINROLE") .build(); return new InMemoryUserDetailsManager(user, admin); } }
Our two hard-coded users, NormalUser and Administrator, only serve the example. In the real thing, a UserDetailsManager should, of course, look different.
Annotations for view classes
The following annotations restrict access to our views. If no annotation is assigned, then @DenyAll will automatically apply, which completely restricts access to the view for all users.
@PermitAll allow access to a single view for all (authenticated) users:
@Route(value = "private", layout = MainView.class) @PageTitle("A private View") @PermitAll public class PrivateView extends VerticalLayout { // ... }
@RolesAllowed allows access to a view for all defined user roles:
@Route(value = "admin", layout = MainView.class) @PageTitle("A view only for admins") @RolesAllowed("ADMINROLE") public class AdminView extends VerticalLayout { // ... }
Note that the user roles are also case-sensitive!
@AnonymousAllowed allows access to a view for everyone, even for not authenticated users. This annotation should be used carefully and only for public views:
@Route(value = "", layout = MainView.class) @PageTitle("A public view") @AnonymousAllowed public class PublicView extends VerticalLayout { // ... }
When using these annotations, one should remember that they are inheritable. This means that they are passed on from a parent class to a child class. One can, however, overwrite this by annotating the child’s class. Here the following applies: @DenyAll overwrites all other annotations, @AnonymousAllowed overwrites @RolesAllowed and @PermitAll, while @RolesAllowed overwrites @PermitAll.
Conclusion
These steps give our users the means to log in and -out again. We also learned how to specifically restrict the access rights for each of our views.