【问题标题】:How to secure Vaadin flow application with Spring Security如何使用 Spring Security 保护 Vaadin 流应用程序
【发布时间】:2019-04-02 15:47:25
【问题描述】:

我正在尝试将 vaadin 10 与 spring security 集成(使用 vaadin 提供的 spring 项目库),但我对它们之间的交互方式感到困惑。如果我直接在浏览器中输入受保护的 url(在本例中为“/about”),则会显示登录页面。如果我通过单击 UI 中的链接访问相同的 URL,即使我未通过身份验证,该页面也会显示。所以我猜 Vaadin 没有通过 Spring Security 的过滤器链,但是我如何保护 UI 内的资源,以及如何在 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

}

【问题讨论】:

    标签: java spring spring-security vaadin10


    【解决方案1】:

    使用 Vaadin Flow (12.0.2)、Spring Boot Starter (2.0.2.RELEASE) 和 Spring Boot Security,基本上,我发现使用以下方式基于角色/权限进行授权;

    基于路由/上下文的角色/权限管理

    • Spring 安全性 (HttpSecurity)
    • Vaadin API(BeforeEnterListener 和 Route/Navigation 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-utils 视图之前检查用户是否具有 ADMIN 角色的示例;

    @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 角色,(s)他将被路由到 Spring Security 配置中已经允许所有人使用的 DeniedAccessView。

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

    在上面的示例(AdminUtilsView)中,您还可以通过自动装配 HttpServletRequest 在 Vaadin 代码中看到 HttpServletRequest.isUserInRole() 的用例。

    总结:如果你的view有Route,先使用BeforeEnterListener授权请求,否则使用Spring Security 用于休息服务等的匹配器(例如 regexMatchers 或 antMatchers)。

    注意: 将 Vaadin Route 和 Spring Security 匹配器规则一起用于同一规则可能有点扭曲,我不建议这样做(它会导致 Vaadin 中的一些内部循环;例如,想象一下我们有一个使用 /view 路由的视图和一个具有所需角色的 /view 条目在 Spring Security 中。如果用户缺少这样的角色并且(s)他被路由/导航到这样的页面(使用 Vaadin 路由 API),Vaadin 尝试以打开与路由关联的视图,而 Spring 安全性会因缺少角色而避免这种情况)。

    另外,我认为,在将用户重新路由或导航到不同的视图/上下文之前,使用 Vaadin 流导航 API 是一种很好的做法,即检查所需的角色/权限。

    此外,为了有一个在 Vaadin 中使用 AuthenticationManager 的示例,我们可以有一个基于 Vaadin 的 LoginView,类似于:

    @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 UserDetailsS​​ervice 完成角色管理并创建 PasswordEncoder bean:

    【讨论】:

    • 哇,很好的答案。不能更直接地使用 Spring Security 太糟糕了,但这是一个很好的直接解决方案。稍后我会在自己的测试项目中尝试一下,但它看起来像是我正在寻找的答案
    • 非常好的答案对我有用。我发现这个教程看起来也很有帮助vaadin.com/tutorials/securing-your-app-with-spring-security
    猜你喜欢
    • 2017-04-14
    • 2020-04-06
    • 2011-09-10
    • 1970-01-01
    • 2018-02-22
    • 1970-01-01
    • 1970-01-01
    • 2019-09-08
    • 2021-12-22
    相关资源
    最近更新 更多