如何使用 Spring Security 保护 Vaadin 流应用程序

Posted

技术标签:

【中文标题】如何使用 Spring Security 保护 Vaadin 流应用程序【英文标题】:How to secure Vaadin flow application with Spring Security 【发布时间】: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


【问题讨论】:

【参考方案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://***.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:

https://github.com/igor-baiborodine/vaadin-demo-bakery-app/blob/master/src/main/java/com/kiroule/vaadin/bakeryapp/app/security/SecurityConfiguration.java https://github.com/igor-baiborodine/vaadin-demo-bakery-app/blob/master/src/main/java/com/kiroule/vaadin/bakeryapp/app/security/UserDetailsServiceImpl.java https://www.baeldung.com/role-and-privilege-for-spring-security-registration

【讨论】:

哇,很好的答案。不能更直接地使用 Spring Security 太糟糕了,但这是一个很好的直接解决方案。稍后我会在自己的测试项目中尝试一下,但它看起来像是我正在寻找的答案 非常好的答案对我有用。我发现这个教程看起来也很有帮助vaadin.com/tutorials/securing-your-app-with-spring-security

以上是关于如何使用 Spring Security 保护 Vaadin 流应用程序的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Spring Security 保护 Vaadin 流应用程序

如何使用 Spring Security 保护 REST Web 服务

如何使用 Spring Security Oauth 保护 Struts2 Rest 服务

如何使用 Spring Security OAuth2 和 MITREID Connect Introspect 保护资源?

Grails、Spring Security 和 Angular JS - 如何保护 URL?

如何使用 Spring Security OAuth2 提供程序配置指定要保护的资源