Securing Spring Boot Applications
Vaadin is fully compatible with the most-popular security solutions in the Java ecosystem, including but not limited to Spring Security, Java Authentication and Authorization Service (JAAS), and Apache Shiro. And although you can choose the security framework you want to use, Vaadin Flow comes with built-in security helpers that are most convenient to implement with Spring Security. Using these built-in helpers with Spring Security makes it easier, faster, and safer to secure your Vaadin applications than if you were use the Spring Security framework directly, or if you were to use another security framework.
This guide focuses on enabling security using the built-in helpers in combination with Spring Boot and Spring Security. For instructions on using the built-in helpers in non-Spring projects, see this separate chapter.
Introduction
In a Spring Boot application, Vaadin Flow built-in security helpers enable a view-based access control mechanism with minimum Spring Security configurations.
This view-based access control mechanism enables you to fully secure views in Vaadin Flow applications in a flexible way, based on different access level annotations.
Specifically, the view-based access control mechanism uses the @AnonymousAllowed
, @PermitAll
, @RolesAllowed
, and @DenyAll
annotations on view classes to define the access control rules.
To enable the mechanism in a Vaadin Flow Spring Boot application without any security, add the following to the project (if they don’t exist already):
-
A Login view.
-
Spring Security dependencies.
-
Log-out capability.
-
A security configuration class that extends
VaadinWebSecurity
. -
One of the following annotations on each view class:
@AnonymousAllowed
,@PermitAll
, or@RolesAllowed
.
A complete example project, including the source code from this document, is available at github.com/vaadin/flow-crm-tutorial
.
Log-in View
Having a log-in view is a basic requirement of many authentication and authorization mechanisms, to be able to redirect anonymous users to that page before giving access to view any protected resources. The log-in view should always be accessible by anonymous users.
@Route("login")
@PageTitle("Login")
@AnonymousAllowed
public class LoginView extends VerticalLayout implements BeforeEnterObserver {
private LoginForm login = new LoginForm();
public LoginView() {
addClassName("login-view");
setSizeFull();
setJustifyContentMode(JustifyContentMode.CENTER);
setAlignItems(Alignment.CENTER);
login.setAction("login");
add(new H1("Test Application"), login);
}
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
if(beforeEnterEvent.getLocation()
.getQueryParameters()
.getParameters()
.containsKey("error")) {
login.setError(true);
}
}
}
Note
|
Usage of Vaadin’s Login Form component
This example uses Vaadin’s Login Form component for the sake of brevity in this implementation.
However, there is no obligation to do so; feel free to implement your own log-in view, as you wish.
|
Spring Security Dependencies
To enable Spring Security, there are certain dependencies that should be added to the project. As this guide is based on Spring Boot, you should add the following dependency:
<dependencies>
<!-- other dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- other dependencies -->
</dependencies>
Log-Out Capability
To handle log-out in web applications, one typically uses a log-out button. Here is a basic implementation of a log-out button shown on the header of the main layout:
public class MainLayout extends AppLayout {
private SecurityService securityService;
public MainLayout(@Autowired SecurityService securityService) {
this.securityService = securityService;
H1 logo = new H1("Vaadin CRM");
logo.addClassName("logo");
HorizontalLayout header;
if (securityService.getAuthenticatedUser() != null) {
Button logout = new Button("Logout", click ->
securityService.logout());
header = new HorizontalLayout(logo, logout);
} else {
header = new HorizontalLayout(logo);
}
// Other page components omitted.
addToNavbar(header);
}
}
The method of getting the authenticated user and logging the user out may vary from application to application. Here is a basic example of doing this with the Spring Security API:
@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);
}
}
Security Utilities
To access authenticated user details and to simplify logout handling, Vaadin provides an AuthenticationContext
component,
strictly integrated with Spring Security, that can be injected into views and services.
The AuthenticationContext
by design does not implement java.io.Serializable
, so Vaadin view fields referencing this object must be defined transient
.
The class exposes following utility methods:
-
isAuthenticated()
: checks if a user is currently logged in. SpringAnonymous
user is considered not authenticated. -
getAuthenticatedUser(Class<U> userType)
: gets user details. IfuserType
does not match the actual user implementation, the methods throws aClassCastException
. -
logout
: initiates the Spring Security logout process and redirects the user to the configured logout URL.
Here is an example implementation of a log-out button shown on the header of the main layout that uses the AuthenticationContext
component:
public class MainLayout extends AppLayout {
private final transient AuthenticationContext authContext;
public MainLayout(AuthenticationContext authContext) {
this.authContext = authContext;
H1 logo = new H1("Vaadin CRM");
logo.addClassName("logo");
HorizontalLayout
header =
authContext.getAuthenticatedUser(UserDetails.class)
.map(user -> {
Button logout = new Button("Logout", click ->
this.authContext.logout());
Span loggedUser = new Span("Welcome " + user.getUsername());
return new HorizontalLayout(logo, loggedUser, logout);
}).orElseGet(() -> new HorizontalLayout(logo));
// Other page components omitted.
addToNavbar(header);
}
}
Security Configuration Class
The next step is to have a Spring Security class that extends VaadinWebSecurity
.
There’s no convention for naming this class, so in this documentation it’s named SecurityConfiguration
.
However, take care with Spring Security annotations.
Here is a minimal implementation of such a class:
@EnableWebSecurity // (1)
@Configuration
public class SecurityConfiguration
extends VaadinWebSecurity { // (2)
@Override
protected void configure(HttpSecurity http) throws Exception {
// Delegating the responsibility of general configurations
// of http security to the super class. It's configuring
// the followings: Vaadin's CSRF protection by ignoring
// framework's internal requests, default request cache,
// ignoring public views annotated with @AnonymousAllowed,
// restricting access to other views/endpoints, and enabling
// ViewAccessChecker authorization.
// You can add any possible extra configurations of your own
// here (the following is just an example):
// http.rememberMe().alwaysRemember(false);
// Configure your static resources with public access before calling
// super.configure(HttpSecurity) as it adds final anyRequest matcher
http.authorizeRequests().antMatchers("/public/**")
.permitAll();
super.configure(http); // (3)
// This is important to register your login view to the
// view access checker mechanism:
setLoginView(http, LoginView.class); // (4)
}
@Override
public void configure(WebSecurity web) throws Exception {
// Customize your WebSecurity configuration.
super.configure(web);
}
/**
* Demo UserDetailsManager which only provides two hardcoded
* in memory users and their roles.
* NOTE: This shouldn't be used in real world applications.
*/
@Bean
public UserDetailsManager userDetailsService() {
UserDetails user =
User.withUsername("user")
.password("{noop}user")
.roles("USER")
.build();
UserDetails admin =
User.withUsername("admin")
.password("{noop}admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
-
Notice the presence of
@EnableWebSecurity
and@Configuration
annotations on top of the above class. As their names imply, they tell Spring to enable its security features. -
VaadinWebSecurity
is a helper class that configures the common Vaadin-related Spring security settings. By extending it, the view-based access control mechanism is enabled automatically, and no further configuration is needed to enable it. Other benefits are covered as follows. -
The default implementation of the
configure
methods takes care of all the Vaadin-related configuration, for example ignoring static resources, or enablingCSRF
checking, while ignoring unnecessary checking for Vaadin internal requests, etc. -
The log-in view can be configured via the provided
setLoginView()
method.
Warning
|
Never use hard-coded credentials in production
The implementation of the userDetailsService() method is just an in-memory implementation for the sake of brevity in this documentation.
In a real-world application, you can change the Spring Security configuration to use an authentication provider for Lightweight Directory Access Protocol (LDAP), JAAS, and other real-world sources.
Read more about Spring Security authentication providers.
|
The most important configuration in the above example is the call to setLoginView(http, LoginView.class)
inside the first configure method.
This is how the view-based access control mechanism knows where to redirect users when they try to navigate to a protected view.
The log-in view should always be accessible by anonymous users, so it should have the @AnonymousAllowed
annotation.
This is especially important when using the variant of the setLoginView
method where you provide the route path (although this signature is meant to be used with Hilla views, not with Flow views).
Note
|
Component-based security configuration
Spring Security 5.7.0 deprecates the WebSecurityConfigurerAdapter and encourages users to move towards a component-based security configuration.
|
VaadinWebSecurityConfigurerAdapter
is still available for Vaadin 23.2 users, although it’s recommended to use component-based security configuration as in SecurityConfiguration
example above.
Read more about updating from WebSecurityConfigurerAdapter to component-based security configuration.
Now that the LoginView
is ready, and it’s set as the log-in view in the security configuration, it’s time to move forward and see how the security annotations work on the views.
Annotating the View Classes
Before providing some usage examples of access annotations, it would be useful to have a closer look at the annotations, and their meaning when applied to a view:
-
@AnonymousAllowed
permits anyone to navigate to the view without any authentication or authorization. -
@PermitAll
allows any authenticated user to navigate to the view. -
@RolesAllowed
grants access to users having the roles specified in the annotation value. -
@DenyAll
disallows everyone from navigating to the view. This is the default, which means that, if a view isn’t annotated at all, the@DenyAll
logic is applied.
When the security configuration class extends from VaadinWebSecurityConfigurerAdapter
, Vaadin’s SpringSecurityAutoConfiguration
comes into play and enables the view-based access control mechanism.
Therefore, none of the views are accessible until one of these annotations (except @DenyAll
) is applied to them.
Some examples:
@Route(value = "", layout = MainView.class)
@PageTitle("Public View")
@AnonymousAllowed
public class PublicView extends VerticalLayout {
// ...
}
@Route(value = "private", layout = MainView.class)
@PageTitle("Private View")
@PermitAll
public class PrivateView extends VerticalLayout {
// ...
}
@Route(value = "admin", layout = MainView.class)
@PageTitle("Admin View")
@RolesAllowed("ADMIN") // <- Should match one of the user's roles (case-sensitive)
public class AdminView extends VerticalLayout {
// ...
}
@RolesAllowed("ADMIN")
public abstract class AbstractAdminView extends VerticalLayout {
// ...
}
@Route(value = "user-listing", layout = MainView.class)
@PageTitle("User Listing")
public class UserListingView extends AbstractAdminView {
// ...
}
As shown in the last example, the security annotations are inherited from the closest parent class that has them. Annotating a child class overrides any inherited annotations. Interfaces aren’t checked for annotations, only classes. By design, the annotations aren’t read from parent layouts or "parent views", as this would make it unnecessarily complex to determine which security level should be applied. If multiple annotations are specified on a single view class, the following rules are applied:
-
DenyAll
overrides other annotations -
AnonymousAllowed
overridesRolesAllowed
andPermitAll
-
RolesAllowed
overridesPermitAll
However, specifying more than one of the above access annotations on a view class isn’t recommended, as it’s confusing and there is probably no logical reason to do so.
If the user is already authenticated and tries to navigate to a view for which they don’t have permission, an error message is displayed. The message depends on the application mode:
-
In development mode, Vaadin shows an Access denied message with a list of available routes.
-
In production mode, Vaadin shows the
RouteNotFoundError
view, which shows a Could not navigate to 'RequestedRouteName' message by default. For security reasons, the message doesn’t say whether the navigation target exists.
Limitations
Mixing any of the view access annotations with Spring’s URL-based HTTP security (which possibly exists in older Vaadin Spring Boot applications) may result in unwanted access configurations or unnecessary complications.
Important
|
Don’t mix Spring’s URL-based HTTP security and view-based access control on a single view
Vaadin strongly recommends not mixing Spring’s URL-pattern-based HTTP security and this view-based access control mechanism targeting the same views, since it may lead to unwanted access configurations, and would be an unnecessary complication in the authorization of views.
|
4C8D835D-4E6E-4D81-BEA1-A865FEB17BAD