Vaadin/Spring Boot中如何解决闲置会话过期后HTTP会话未重置问题?
在Vaadin+Okta认证应用中正确实现闲置会话过期
问题核心
集成Okta OAuth2认证的Vaadin应用中,当达到server.servlet.session.timeout设置的闲置时长后,Vaadin会显示自定义会话过期对话框,但用户点击窗口或按ESC后,HTTP会话(JSESSIONID)并未被终止或重建,系统检测到有效Okta会话会自动完成登录,无法触发重新认证流程。后续添加server.servlet.session.cookie.max-age配置解决了会话不刷新的问题,但该配置会强制用户重新登录,无法识别用户的活动状态。
当前配置
SecurityConfiguration
@EnableWebSecurity @Configuration @Order(99) public class S1SecurityConfiguration extends SecurityConfiguration { ... @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); // 应用默认配置,处理匿名用户 http.oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.oidcUserService(oidcUserService())) .authorizationEndpoint(authEndpoint -> authEndpoint .authorizationRequestResolver( new ForcePromptLoginRequestResolver( clientRegistrationRepository, "/oauth2/authorization" ) ) ) .successHandler(s1authSuccessHandler) .failureHandler(authFailureHandler) ); // 启用会话并发管理 http.sessionManagement(sessionManagement -> sessionManagement .sessionFixation(sessionFixation -> sessionFixation.migrateSession()) .sessionConcurrency(sessionConcurrency -> sessionConcurrency .sessionRegistry(sessionRegistry()) .maximumSessions(1) .maxSessionsPreventsLogin(false) // 允许第二次登录,同时使第一次登录的会话失效 .expiredUrl("/session-expired") // 会话过期时跳转的页面 ) ); ... } @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } @Bean public SessionAuthenticationStrategy sessionAuthenticationStrategy(SessionRegistry sessionRegistry) { // 并发会话控制策略 ConcurrentSessionControlAuthenticationStrategy concurrencyStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry); concurrencyStrategy.setMaximumSessions(1); concurrencyStrategy.setExceptionIfMaximumExceeded(true); // 等同于maxSessionsPreventsLogin(true) // 组合会话固定保护与并发控制策略 return new CompositeSessionAuthenticationStrategy( Arrays.asList( new ChangeSessionIdAuthenticationStrategy(), // 也可使用MigrateSession concurrencyStrategy ) ); } @Bean public S1AuthenticationSuccessHandler s1authFailureHandler(SessionAuthenticationStrategy sessionAuthenticationStrategy) { return new S1AuthenticationSuccessHandler(sessionAuthenticationStrategy); } @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } @Bean public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { final OidcUserService delegate = new OidcUserService(); return userRequest -> { // 通过委托加载默认OidcUser OidcUser oidcUser = delegate.loadUser(userRequest); String principalName = oidcUser.getAttribute("email"); // 返回自定义SimpleOidcUser,仅用邮箱做 equality 校验,保证并发检查生效 return new SimpleOidcUser( oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo(), principalName ); }; } }
application.properties
server.servlet.session.timeout=30m # 设置closeIdleSessions为true,避免心跳/推送请求重置会话闲置计时器 vaadin.closeIdleSessions=true
SessionExpiredMessageInitServiceListener
@Component public class SessionExpiredMessageInitServiceListener implements VaadinServiceInitListener { @Override public void serviceInit(ServiceInitEvent event) { event.getSource().setSystemMessagesProvider(new SystemMessagesProvider() { @Override public CustomizedSystemMessages getSystemMessages(SystemMessagesInfo systemMessagesInfo) { CustomizedSystemMessages messages = new CustomizedSystemMessages(); messages.setSessionExpiredCaption("Session expired"); messages.setSessionExpiredMessage( "Your session has expired. Press ESC or click anywhere in this window to continue." ); // 会话过期跳转的URL messages.setSessionExpiredURL("/envdata"); messages.setSessionExpiredNotificationEnabled(true); return messages; } }); } }
解决方案
1. 同步销毁Vaadin会话与HTTP会话
添加Vaadin会话销毁监听器,当Vaadin会话因闲置过期被销毁时,同步销毁对应的HTTP会话:
@Component public class VaadinSessionDestroyListener implements VaadinSessionDestroyListener { @Override public void sessionDestroy(VaadinSessionDestroyEvent event) { HttpServletRequest request = VaadinServletRequest.getCurrent().getHttpServletRequest(); if (request != null) { // 销毁HTTP会话,清除JSESSIONID request.getSession().invalidate(); } } }
2. 修改会话过期跳转逻辑,强制触发重新认证
创建一个专门的端点用于销毁会话并跳转至Okta认证入口,替换原有的sessionExpiredURL:
// 在SecurityConfiguration中添加端点 @GetMapping("/session-expired-handler") public String handleSessionExpired(HttpServletRequest request) { // 销毁当前HTTP会话 request.getSession().invalidate(); // 跳转至Okta认证入口,触发重新登录 return "redirect:/oauth2/authorization/okta"; }
然后更新SessionExpiredMessageInitServiceListener中的跳转URL:
messages.setSessionExpiredURL("/session-expired-handler");
3. 移除server.servlet.session.cookie.max-age配置
该配置会强制Cookie在指定时间后过期,忽略用户的活动状态,直接移除即可恢复基于闲置时长的会话过期逻辑。
4. 优化Spring Security会话管理
确保会话过期后,Spring Security能正确清除认证上下文:
- 检查
HttpSessionEventPublisher是否正常工作,它负责向Spring Security发布会话销毁事件,确保SessionRegistry能及时更新会话状态。 - 确认
sessionConcurrency配置中的expiredUrl指向上述的/session-expired-handler端点,保证会话过期后统一处理。
验证步骤
- 设置较短的会话超时时间(如1分钟)进行测试。
- 等待会话过期,确认Vaadin显示自定义对话框。
- 点击窗口或按ESC,确认被跳转至Okta登录页面,而非自动登录。
- 登录后检查新的JSESSIONID是否与之前不同,确保会话已重建。
内容的提问来源于stack exchange,提问作者Georgi Koemdzhiev
相关产品推荐
相关产品推荐

