Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Вход и права доступа

Админку нельзя оставлять открытой: смотреть статьи может любой сотрудник, а создавать и удалять — только администратор. Это две задачи: аутентификация (кто вы — вход по логину и паролю) и авторизация (что вам можно — права по ролям). Обе решает 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).

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.