You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vaadin 23.2.5应用重新部署后无法完整重载问题求助

Vaadin 23.2.5 生产环境重部署后加载停滞问题解决

问题描述

生产环境重部署后,应用无法完成重载,顶部持续显示蓝色进度条;浏览器控制台报以下JS错误:

  • FireFox:Cannot read properties of undefined (reading 'length')
  • Chrome:Cannot read properties of undefined (reading '0')
    已移除应用中所有Grid组件,问题仍未解决。

配置代码

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter {

    private final ClientRegistrationRepository clientRegistrationRepository;
    private final GrantedAuthoritiesMapper authoritiesMapper;
    private final ProfileService profileService;

    SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository,
                          GrantedAuthoritiesMapper authoritiesMapper, ProfileService profileService) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authoritiesMapper = authoritiesMapper;
        this.profileService = profileService;
        SecurityContextHolder.setStrategyName(VaadinAwareSecurityContextHolderStrategy.class.getName());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                // Enable OAuth2 login
                .oauth2Login(oauth2Login ->
                        oauth2Login
                                .clientRegistrationRepository(clientRegistrationRepository)
                                .userInfoEndpoint(userInfoEndpoint ->
                                        userInfoEndpoint
                                                // Use a custom authorities mapper to get the roles from the identity provider into the Authentication token
                                                .userAuthoritiesMapper(authoritiesMapper)
                                )
                                // Use a Vaadin aware authentication success handler
                                .successHandler(new KeycloakVaadinAuthenticationSuccessHandler(profileService))
                )
                // Configure logout
                .logout(logout ->
                        logout
                                // Enable OIDC logout (requires that we use the 'openid' scope when authenticating)
                                .logoutSuccessHandler(logoutSuccessHandler())
                                // When CSRF is enabled, the logout URL normally requires a POST request with the CSRF
                                // token attached. This makes it difficult to perform a logout from within a Vaadin
                                // application (since Vaadin uses its own CSRF tokens). By changing the logout endpoint
                                // to accept GET requests, we can redirect to the logout URL from within Vaadin.
                                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                );
    }

    @Bean
    @Primary
    public SpringViewAccessChecker springViewAccessChecker(AccessAnnotationChecker accessAnnotationChecker) {
        return new KeycloakSpringViewAccessChecker(accessAnnotationChecker, "/oauth2/authorization/keycloak");
    }

    private OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler() {
        var logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
        return logoutSuccessHandler;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        // Don't apply security rules on our static pages
        web.ignoring().antMatchers("/session-expired", "/images/*");
    }

    @Bean
    public PolicyFactory htmlSanitizer() {
        // This is the policy we will be using to sanitize HTML input
        return Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.STYLES).and(Sanitizers.LINKS);
    }

}


@Component
class VaadinSessionConfiguration implements VaadinServiceInitListener, SystemMessagesProvider, SessionDestroyListener {

    private final String relativeSessionExpiredUrl;

    VaadinSessionConfiguration(ServerProperties serverProperties) {
        relativeSessionExpiredUrl = UriComponentsBuilder.fromPath(serverProperties.getServlet().getContextPath()).path("logout").build().toUriString();
    }

    @Override
    public SystemMessages getSystemMessages(SystemMessagesInfo systemMessagesInfo) {
        var messages = new CustomizedSystemMessages();
        // Redirect to a specific screen when the session expires. In this particular case we don't want to logout
        // just yet. If you would like the user to be completely logged out when the session expires, this URL
        // should the logout URL.
        messages.setSessionExpiredURL(relativeSessionExpiredUrl);
        return messages;
    }

    @Override
    public void sessionDestroy(SessionDestroyEvent event) {
        // We also want to destroy the underlying HTTP session since it is the one that contains the authentication
        // token.
        try {
            event.getSession().getSession().invalidate();
        } catch (Exception ignore) {
            // Session was probably already invalidated.
        }
    }

    @Override
    public void serviceInit(ServiceInitEvent event) {
        event.getSource().setSystemMessagesProvider(this);
        event.getSource().addSessionDestroyListener(this);
    }

}

public final class VaadinAwareSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

    private final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

    @Override
    public void clearContext() {
        contextHolder.remove();
    }

    @Override
    @NonNull
    public SecurityContext getContext() {
        var context = contextHolder.get();
        if (context == null) {
            context = getFromVaadinSession().orElseGet(() -> {
                var newCtx = createEmptyContext();
                // This copies the behaviour of ThreadLocalSecurityContextHolder.
                contextHolder.set(newCtx);
                return newCtx;
            });
        }
        return context;
    }

    @NonNull
    private Optional<SecurityContext> getFromVaadinSession() {
        // Don't store this security context in the ThreadLocal as that may lead to the context leaking
        // into other sessions as threads may be reused.
        var session = VaadinSession.getCurrent();
        if (session == null) {
            return Optional.empty();
        }
        var securityContext = session.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
        if (securityContext instanceof SecurityContext) {
            return Optional.of((SecurityContext) securityContext);
        } else {
            return Optional.empty();
        }
    }

    @Override
    public void setContext(@NonNull SecurityContext securityContext) {
        contextHolder.set(requireNonNull(securityContext));
    }

    @Override
    @NonNull
    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

补充截图(更新1)

更新1截图


解决方案

1. 修复客户端缓存不兼容问题

在application.properties中开启Vaadin资源缓存破坏机制,确保重部署后客户端加载新资源:

vaadin.frontend.cache.buster=true

部署时同步清理反向代理/CDN缓存,用户端需强制刷新浏览器(Ctrl+F5)。

2. 完善Spring Security资源放行规则

当前配置未放行Vaadin核心静态资源,导致加载失败。修改WebSecurity配置:

@Override
public void configure(WebSecurity web) throws Exception {
    super.configure(web);
    web.ignoring().antMatchers(
        "/VAADIN/**",
        "/frontend/**",
        "/icons/**",
        "/session-expired",
        "/images/*"
    );
}

3. 修正SecurityContext获取逻辑

自定义的VaadinAwareSecurityContextHolderStrategy在资源加载阶段(VaadinSession未创建时)可能引发上下文异常,调整getFromVaadinSession()方法:

@NonNull
private Optional<SecurityContext> getFromVaadinSession() {
    var session = VaadinSession.getCurrent();
    if (session == null) {
        return Optional.empty();
    }
    var httpSession = session.getSession();
    if (httpSession == null) {
        return Optional.empty();
    }
    var securityContext = httpSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
    if (securityContext instanceof SecurityContext) {
        return Optional.of((SecurityContext) securityContext);
    } else {
        return Optional.empty();
    }
}

4. 清理重部署残留会话

在服务器配置中关闭会话持久化(如Tomcat设置sessionPersist=false),避免旧会话与新部署应用不匹配;或在应用启动时添加旧会话清理逻辑。

5. 升级Vaadin版本

Vaadin 23.2.5存在已知部署相关bug,建议升级至23.x最新稳定版(如23.3.24),官方已修复此类兼容问题。


内容的提问来源于stack exchange,提问作者alexanoid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 20:15:33