Vaadin自定义登录页登录后跳转自动登出问题求助
我按照Vaadin官方教程使用Vaadin登录组件时功能正常,但自定义登录表单后,登录成功跳转至其他页面会出现自动登出的情况,需要排查解决。相关代码如下:
登录视图类
@Route("login") @PageTitle("Login") @AnonymousAllowed public class LoginViewOverLay extends Div implements BeforeEnterObserver, ComponentEventListener<AbstractLogin.LoginEvent> { @Autowired private AuthenticationManager authenticationManager; @Autowired SecurityService securityservice; @Autowired UserDetailsServiceImpl userdetails; FormLayout form = new FormLayout(); LoginOverlay loginOverlay = new LoginOverlay(); public LoginViewOverLay() { add(loginOverlay); loginOverlay.setOpened(true); loginOverlay.addLoginListener(this); } @Override public void onComponentEvent(AbstractLogin.LoginEvent loginEvent) { try { securityservice.authenticateUser(loginEvent.getUsername(), loginEvent.getPassword()); loginOverlay.close(); getUI().ifPresent(ui -> ui.navigate("/")); } catch (Exception e) { e.printStackTrace(); } } @Override public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { if(beforeEnterEvent.getLocation() .getQueryParameters() .getParameters() .containsKey("error")) { loginOverlay.setError(true); } } }
SecurityService类
@Component public class SecurityService { @Autowired UserDetailsServiceImpl userdetails; @Autowired PasswordEncoder encoder; private final AuthenticationContext authenticationContext; public SecurityService(AuthenticationContext authenticationContext) { this.authenticationContext = authenticationContext; } public UserDetails getAuthenticatedUser() { return authenticationContext.getAuthenticatedUser(UserDetails.class).get(); } public void logout() { authenticationContext.logout(); } public UserDetails getAuthenticatedUser2() { SecurityContext context = SecurityContextHolder.getContext(); Object principal = context.getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { return (UserDetails) context.getAuthentication().getPrincipal(); } // Anonymous or no authentication. return null; } public void authenticateUser(UserDetails userDetails) { Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } public void authenticateUser(String username, String password) { UserDetails userDetails = userdetails.loadUserByUsername(username); if (encoder.matches(password, userDetails.getPassword())) { Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } else { // System.out.println("wrong"); } } }
UserDetails实现类
@Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; public UserDetailsServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserLogin user = userRepository.findByUserNameAndEnabled(username, true); if (user == null) { throw new UsernameNotFoundException("No user present with username: " + username); } else { //System.out.println("Yes User"); return new User(user.getUserName(), user.getHashedPassword(), getAuthorities(user)); } } private static List<GrantedAuthority> getAuthorities(UserLogin user) { return user.getRoles().stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName())) .collect(Collectors.toList()); } }
安全配置类
@EnableWebSecurity @Configuration public class SecurityConfiguration extends VaadinWebSecurity { @Autowired UserDetailsServiceImpl userdetails; @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/images/*.png")).permitAll() .requestMatchers(new AntPathRequestMatcher("/**", HttpMethod.DELETE.toString())).denyAll() .requestMatchers(new AntPathRequestMatcher("/**", HttpMethod.OPTIONS.toString())).denyAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.OPTIONS, "/**")).denyAll() ).sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).invalidSessionUrl("/") .maximumSessions(1).sessionRegistry(sessionRegistry()).expiredUrl("/") .maxSessionsPreventsLogin(false)); super.configure(http); setLoginView(http, LoginViewOverLay.class); } @Bean SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } }
问题根源
你在SecurityService的authenticateUser方法里直接用SecurityContextHolder.getContext().setAuthentication(authentication)设置认证信息,但这种方式没把认证信息持久化到HttpSession中。Vaadin的安全机制依赖Spring Security的会话持久化,页面跳转时新请求会重新创建SecurityContext,导致之前的认证信息丢失,看起来就像自动登出了。
修复方案
1. 改用AuthenticationManager完成认证
不要手动构造UsernamePasswordAuthenticationToken,而是通过AuthenticationManager的authenticate方法处理认证,这个方法会自动把认证信息绑定到会话里。
修改SecurityService的authenticateUser(String username, String password)方法:
@Autowired private AuthenticationManager authenticationManager; public void authenticateUser(String username, String password) { UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 自动调用UserDetailsService加载用户、验证密码 Authentication authentication = authenticationManager.authenticate(authRequest); SecurityContextHolder.getContext().setAuthentication(authentication); }
2. 移除手动密码校验逻辑
原来的手动加载用户、密码匹配代码可以删掉,AuthenticationManager会自动处理这些逻辑,认证失败会抛出BadCredentialsException等异常,你可以在登录视图里捕获这些异常并提示错误。
修改登录视图的onComponentEvent方法:
@Override public void onComponentEvent(AbstractLogin.LoginEvent loginEvent) { try { securityservice.authenticateUser(loginEvent.getUsername(), loginEvent.getPassword()); loginOverlay.close(); getUI().ifPresent(ui -> ui.navigate("/")); } catch (BadCredentialsException e) { // 密码错误或用户不存在,显示登录错误 loginOverlay.setError(true); } catch (Exception e) { e.printStackTrace(); loginOverlay.setError(true); } }
3. 清理无用依赖
登录视图里注入的AuthenticationManager和UserDetailsServiceImpl现在用不上了,可以直接移除,减少不必要的耦合。
额外说明
你的安全配置类里的会话管理配置是合理的,SessionCreationPolicy.IF_REQUIRED会在需要时创建会话,不需要修改。
内容的提问来源于stack exchange,提问作者Mei Mariang

