Login and access rights
An admin panel must not stay open: any employee may read posts, but only an administrator may create and delete them. These are two tasks: authentication (who you are — logging in with a username and password) and authorization (what you may do — permissions by role). Spring Security handles both, and Vaadin adds permission checks at the view level.
The dependency
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>After this Vaadin closes all views: only views with an access annotation can be opened.
Security configuration
// src/main/java/com/example/blog/SecurityConfig.java
package com.example.blog;
import com.example.blog.views.LoginView;
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.with(VaadinSecurityConfigurer.vaadin(), vaadin -> vaadin.loginView(LoginView.class));
return http.build();
}
@Bean
UserDetailsService users() {
var anna = User.withUsername("anna").password("{noop}changeme").roles("ADMIN").build();
var oleg = User.withUsername("oleg").password("{noop}changeme").roles("USER").build();
return new InMemoryUserDetailsManager(anna, oleg);
}
}VaadinSecurityConfigurer sets Spring Security up for Vaadin: it lets internal requests through, turns on CSRF protection and sets the login page. Users are kept in memory for now; {noop} means an unencrypted password — for learning only, a real project stores password hashes in the database.
The login page
// src/main/java/com/example/blog/views/LoginView.java
package com.example.blog.views;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.login.LoginForm;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
@Route(value = "login", autoLayout = false)
@PageTitle("Login")
@AnonymousAllowed
public class LoginView extends VerticalLayout implements BeforeEnterObserver {
private final LoginForm login = new LoginForm();
public LoginView() {
setSizeFull();
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
login.setAction("login");
login.setForgotPasswordButtonVisible(false);
add(new H1("Blog admin"), login);
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) {
login.setError(true);
}
}
}Open http://localhost:8080/ and Vaadin redirects to /login. The form sends the username and password to the login address, which Spring Security handles. With a wrong password it sends you back to /login?error, and beforeEnter shows the error.
The form labels are changed with LoginI18n (import com.vaadin.flow.component.login.LoginI18n):
// src/main/java/com/example/blog/views/LoginView.java (part of the constructor)
LoginI18n.Form form = new LoginI18n.Form();
form.setTitle("Sign in");
form.setSubmit("Sign in");
LoginI18n i18n = new LoginI18n();
i18n.setForm(form);
login.setI18n(i18n);The form title and the button both read "Sign in", and the labels you did not set (such as the username and password fields) keep their defaults. The error text is changed the same way with LoginI18n.ErrorMessage and i18n.setErrorMessage(...).
Permissions on views
| Annotation | Who can open the view |
|---|---|
@AnonymousAllowed | Everyone, even without logging in |
@PermitAll | Any logged-in user |
@RolesAllowed("ADMIN") | Only users with the ADMIN role |
@DenyAll or no annotation | Nobody |
Let us place them: @PermitAll on PostListView and on MainLayout, @RolesAllowed("ADMIN") on PostFormView. The annotations come from the jakarta.annotation.security package, except @AnonymousAllowed from com.vaadin.flow.server.auth.
Log in as oleg with the password changeme: the post list opens, while /posts shows the "route not found" page — Vaadin does not reveal that a closed view exists.
Logging out and the current user
// src/main/java/com/example/blog/views/MainLayout.java (fragment)
public MainLayout(AuthenticationContext auth) {
String user = auth.getPrincipalName().orElse("");
Button logout = new Button("Log out " + user, event -> auth.logout());
SideNav nav = new SideNav();
nav.addItem(new SideNavItem("Posts", PostListView.class, VaadinIcon.LIST.create()));
if (auth.hasRole("ADMIN")) {
nav.addItem(new SideNavItem("New post", PostFormView.class, VaadinIcon.PLUS.create()));
}
}The header gets a "Log out anna" button, and only administrators see the "New post" item. AuthenticationContext is a Vaadin bean, any view can receive it in its constructor.
Common mistake
A view is marked @AnonymousAllowed but still redirects to the login page. The cause is the layout: HelloView gets into MainLayout automatically, and the layout has @PermitAll. Permissions are checked on both the view and its layout. For public pages, switch the layout off: @Route(value = "hello", autoLayout = false).