Как защитить приложение Vaadin Flow с помощью Spring Security

Я пытаюсь интегрировать vaadin 10 с безопасностью Spring (используя базу проекта spring, предоставленную vaadin), и я не понимаю, как именно они взаимодействуют. Если я перейду на защищенный URL-адрес (в этом примере «/ about»), набрав его прямо в браузере, появится страница входа. Если я перейду на тот же URL-адрес, щелкнув ссылку в пользовательском интерфейсе, страница появится, даже если я не аутентифицирован. Итак, я предполагаю, что Vaadin не проходит через цепочку фильтров Spring Security, но тогда как мне защитить свои ресурсы внутри пользовательского интерфейса и как я могу поделиться аутентифицированным пользователем между vaadin и spring? Должен ли я внедрять безопасность дважды? Доступная документация, похоже, не охватывает этого, и каждая ссылка в Интернете содержит примеры с Vaadin 7-8, которые я никогда не использовал и, похоже, работает иначе, чем 10+.

Кто-нибудь знает какой-либо ресурс об этом, или вы можете просветить меня о том, как все это работает вместе, чтобы я мог знать, что делаю?

Вот моя конфигурация безопасности:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String[] ALLOWED_GET_URLS = {
        "/",
        //"/about",
        "/login/**",
        "/frontend/**",
        "/VAADIN/**",
        "/favicon.ico"
    };

    private static final String[] ALLOWED_POST_URLS = {
        "/"
    };

    //@formatter:off
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .disable()
            .authorizeRequests()
                .mvcMatchers(HttpMethod.GET, ALLOWED_GET_URLS)
                    .permitAll()
                .mvcMatchers(HttpMethod.POST, ALLOWED_POST_URLS)
                    .permitAll()
                .anyRequest()
                    .fullyAuthenticated()
             .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
            .and()
                .logout()
                    .logoutSuccessUrl("/")
                    .permitAll();
    }
    //@formatter:on

}

person SrThompson    schedule 29.10.2018    source источник


Ответы (1)


Используя Vaadin Flow (12.0.2), Spring Boot Starter (2.0.2.RELEASE) и Spring Boot Security, я обнаружил, что авторизация на основе роли / полномочий осуществляется следующими способами:

Управление ролями / полномочиями на основе маршрута / контекста

  • Безопасность Spring (HttpSecurity)
  • API Vaadin (BeforeEnterListener и API маршрута / навигации)

Управление ролью / полномочиями бизнес-подразделения

  • Внутри кода с использованием метода HttpServletRequest.isUserInRole

Начнем с простого примера конфигурации Spring Security;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
        extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // CSRF is handled by Vaadin: https://vaadin.com/framework/security
                .exceptionHandling().accessDeniedPage("/accessDenied")
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
                .and().logout().logoutSuccessUrl("/")
                .and()
                .authorizeRequests()
                // allow Vaadin URLs and the login URL without authentication
                .regexMatchers("/frontend/.*", "/VAADIN/.*", "/login.*", "/accessDenied").permitAll()
                .regexMatchers(HttpMethod.POST, "/\\?v-r=.*").permitAll()
                // deny any other URL until authenticated
                .antMatchers("/**").fullyAuthenticated()
            /*
             Note that anonymous authentication is enabled by default, therefore;
             SecurityContextHolder.getContext().getAuthentication().isAuthenticated() always will return true.
             Look at LoginView.beforeEnter method.
             more info: https://docs.spring.io/spring-security/site/docs/4.0.x/reference/html/anonymous.html
             */
        ;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password("$2a$10$obstjyWMAVfsNoKisfyCjO/DNfO9OoMOKNt5a6GRlVS7XNUzYuUbO").roles("ADMIN");// user and pass: admin 
    }

    /**
    * Expose the AuthenticationManager (to be used in LoginView)
    * @return
    * @throws Exception
    */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Как видите, я еще не указал никаких разрешений на основе роли ни в одном из моих маршрутизируемых представлений (с пометкой @Route). Что я сделаю, так это если у меня будет маршрутизируемое представление, я зарегистрирую BeforeEnterListener, когда он (маршрутизируемое представление) будет построено, и проверим там требуемую роль / привилегию.

Ниже приведен пример проверки наличия у пользователя роли ADMIN перед переходом в представление admin-utils;

@Route(value = "admin-utils")
public class AdminUtilsView extends VerticalLayout { 
@Autowired
private HttpServletRequest req;
...
    AdminUtilsView() {
        ...
        UI.getCurrent().addBeforeEnterListener(new BeforeEnterListener() {
            @Override
            public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
                if (beforeEnterEvent.getNavigationTarget() != DeniedAccessView.class && // This is to avoid a
                        // loop if DeniedAccessView is the target
                        !req.isUserInRole("ADMIN")) {
                    beforeEnterEvent.rerouteTo(DeniedAccessView.class);
                }
            }
        });
    }
}

Если у пользователя нет роли ADMIN, он будет перенаправлен на DeniedAccessView, что уже разрешено для всех в конфигурации Spring Security.

@Route(value = "accessDenied")
public class DeniedAccessView
        extends VerticalLayout {
    DeniedAccessView() {
        FormLayout formLayout = new FormLayout();
        formLayout.add(new Label("Access denied!"));
        add(formLayout);
    }
}

В приведенном выше примере (AdminUtilsView) вы также можете увидеть вариант использования HttpServletRequest.isUserInRole () в коде Vaadin, автоматически подключив HttpServletRequest.

РЕЗЮМЕ: Если ваше представление имеет маршрут, используйте BeforeEnterListener для авторизации запроса в первую очередь, в противном случае используйте сопоставители Spring Security (например, regexMatchers или antMatchers) для служб отдыха и т. д.

ПРИМЕЧАНИЕ. Использование правил сопоставления Vaadin Route и Spring Security вместе для одного и того же правила может быть немного искаженным, и я не предлагаю этого (это вызывает некоторые внутренние петли в Vaadin; например, представьте, что у нас есть представление, направленное с помощью / view, и запись в Spring Security для / view с требуемой ролью. Если у пользователя отсутствует такая роль и он перенаправляется / перемещается на такую ​​страницу (с использованием API маршрутизации Vaadin), Vaadin пытается открыть представление, связанное с маршрутом, в то время как безопасность Spring позволяет избежать этого из-за отсутствия роли).

Кроме того, я думаю, что использование API навигации по потоку Vaadin перед перенаправлением или переходом пользователя в другое представление / контекст было бы хорошей практикой, чтобы проверить наличие требуемой роли / полномочий.

Более того, чтобы иметь пример использования AuthenticationManager в Vaadin, у нас может быть LoginView на основе Vaadin, похожий на:

@Route(value = "login")
public class LoginView
        extends FlexLayout implements BeforeEnterObserver {

    private final Label label;
    private final TextField userNameTextField;
    private final PasswordField passwordField;

    /**
    * AuthenticationManager is already exposed in WebSecurityConfig
    */
    @Autowired
    private AuthenticationManager authManager;

    @Autowired
    private HttpServletRequest req;

    LoginView() {
        label = new Label("Please login...");

        userNameTextField = new TextField();
        userNameTextField.setPlaceholder("Username");
        UiUtils.makeFirstInputTextAutoFocus(Collections.singletonList(userNameTextField));

        passwordField = new PasswordField();
        passwordField.setPlaceholder("Password");
        passwordField.addKeyDownListener(Key.ENTER, (ComponentEventListener<KeyDownEvent>) keyDownEvent -> authenticateAndNavigate());

        Button submitButton = new Button("Login");
        submitButton.addClickListener((ComponentEventListener<ClickEvent<Button>>) buttonClickEvent -> {
            authenticateAndNavigate();
        });

        FormLayout formLayout = new FormLayout();
        formLayout.add(label, userNameTextField, passwordField, submitButton);
        add(formLayout);

        // center the form
        setAlignItems(Alignment.CENTER);
        this.getElement().getStyle().set("height", "100%");
        this.getElement().getStyle().set("justify-content", "center");
    }

    private void authenticateAndNavigate() {
        /*
        Set an authenticated user in Spring Security and Spring MVC
        spring-security
        */
        UsernamePasswordAuthenticationToken authReq
                = new UsernamePasswordAuthenticationToken(userNameTextField.getValue(), passwordField.getValue());
        try {
            // Set authentication
            Authentication auth = authManager.authenticate(authReq);
            SecurityContext sc = SecurityContextHolder.getContext();
            sc.setAuthentication(auth);

            /*
            Navigate to the requested page:
            This is to redirect a user back to the originally requested URL – after they log in as we are not using
            Spring's AuthenticationSuccessHandler.
            */
            HttpSession session = req.getSession(false);
            DefaultSavedRequest savedRequest = (DefaultSavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
            String requestedURI = savedRequest != null ? savedRequest.getRequestURI() : Application.APP_URL;

            this.getUI().ifPresent(ui -> ui.navigate(StringUtils.removeStart(requestedURI, "/")));
        } catch (BadCredentialsException e) {
            label.setText("Invalid username or password. Please try again.");
        }
    }

    /**
    * This is to redirect user to the main URL context if (s)he has already logged in and tries to open /login
    *
    * @param beforeEnterEvent
    */
    @Override
    public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        //Anonymous Authentication is enabled in our Spring Security conf
        if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
            //https://vaadin.com/docs/flow/routing/tutorial-routing-lifecycle.html
            beforeEnterEvent.rerouteTo("");
        }
    }
}

И, наконец, вот метод выхода из системы, который можно вызвать из меню или кнопки:

/**
 * log out the current user using Spring security and Vaadin session management
 */
void requestLogout() {
    //https://stackoverflow.com/a/5727444/1572286
    SecurityContextHolder.clearContext();
    req.getSession(false).invalidate();

    // And this is similar to how logout is handled in Vaadin 8:
    // https://vaadin.com/docs/v8/framework/articles/HandlingLogout.html
    UI.getCurrent().getSession().close();
    UI.getCurrent().getPage().reload();// to redirect user to the login page
}

Вы можете продолжить управление ролями с помощью Spring UserDetailsService и создать bean-компонент PasswordEncoder, просмотрев следующие примеры:

person Youness    schedule 05.01.2019
comment
Вау, отличный ответ. Жаль, что вы не можете использовать Spring Security напрямую, но это хорошее простое решение. Позже я попробую это сделать в собственном тестовом проекте, но похоже, что ответ, который я искал - person SrThompson; 08.01.2019
comment
У меня сработал довольно хороший ответ. Я нашел это руководство, которое тоже выглядит полезным vaadin.com/tutorials/securing- ваше приложение с весенней безопасностью - person andrewps; 08.04.2019