Вход и права доступа
Админку нельзя оставлять открытой: смотреть статьи может любой сотрудник, а создавать и удалять — только администратор. Это две задачи: аутентификация (кто вы — вход по логину и паролю) и авторизация (что вам можно — права по ролям). Обе решает Spring Security, а Vaadin добавляет к нему проверку прав на уровне экранов.
Зависимость
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>После этого Vaadin закрывает все экраны: открыть можно только те, у которых есть аннотация доступа.
Настройка безопасности
// 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 настраивает Spring Security под Vaadin: пропускает служебные запросы, включает защиту от CSRF и задаёт страницу входа. Пользователи пока хранятся в памяти; {noop} означает пароль без шифрования — только для учёбы, в настоящем проекте пароли хранят в базе в виде хеша.
Страница входа
// 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("Вход")
@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("Админка блога"), login);
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) {
login.setError(true);
}
}
}Откройте http://localhost:8080/ — Vaadin перенаправит на /login. Форма отправляет логин и пароль на адрес login, который обслуживает Spring Security. При неверном пароле он вернёт на /login?error, и beforeEnter покажет ошибку.
Подписи формы меняет LoginI18n (импорт com.vaadin.flow.component.login.LoginI18n):
// src/main/java/com/example/blog/views/LoginView.java (фрагмент конструктора)
LoginI18n.Form form = new LoginI18n.Form();
form.setTitle("Вход");
form.setSubmit("Войти");
LoginI18n i18n = new LoginI18n();
i18n.setForm(form);
login.setI18n(i18n);Заголовок формы и кнопка станут «Вход» и «Войти», а подписи, которые вы не задали (например, у полей логина и пароля), останутся стандартными. Так же через LoginI18n.ErrorMessage и i18n.setErrorMessage(...) меняется текст ошибки.
Права на экранах
| Аннотация | Кто может открыть |
|---|---|
@AnonymousAllowed | Все, даже без входа |
@PermitAll | Любой вошедший пользователь |
@RolesAllowed("ADMIN") | Только пользователи с ролью ADMIN |
@DenyAll или без аннотации | Никто |
Расставим их: @PermitAll — на PostListView и на MainLayout, @RolesAllowed("ADMIN") — на PostFormView. Аннотации — из пакета jakarta.annotation.security, кроме @AnonymousAllowed из com.vaadin.flow.server.auth.
Войдите как oleg с паролем changeme: список статей открывается, а /posts показывает страницу «маршрут не найден» — Vaadin не раскрывает, что закрытый экран существует.
Выход и текущий пользователь
// src/main/java/com/example/blog/views/MainLayout.java (фрагмент)
public MainLayout(AuthenticationContext auth) {
String user = auth.getPrincipalName().orElse("");
Button logout = new Button("Выйти " + user, event -> auth.logout());
SideNav nav = new SideNav();
nav.addItem(new SideNavItem("Статьи", PostListView.class, VaadinIcon.LIST.create()));
if (auth.hasRole("ADMIN")) {
nav.addItem(new SideNavItem("Новая статья", PostFormView.class, VaadinIcon.PLUS.create()));
}
}В шапке появляется кнопка «Выйти anna», а пункт «Новая статья» видят только администраторы. AuthenticationContext — бин Vaadin, его можно получить в конструкторе любого экрана.
Частая ошибка
Экран помечен @AnonymousAllowed, но всё равно перенаправляет на вход. Причина — макет: HelloView автоматически попадает в MainLayout, а у макета стоит @PermitAll. Права проверяются и у экрана, и у его макета. Для открытых страниц отключите макет: @Route(value = "hello", autoLayout = false).