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

Vaadin自定义登录页登录后跳转自动登出问题求助

自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 07:44:52