Spring Security为何忽略我的登录请求(Vaadin、Postgres环境)
Vaadin LoginForm 集成 Spring Boot Security 登录失败无报错排查与解决
我最近在做Vaadin和Spring Boot Security的集成,用LoginForm.action("login")把登录请求转发给Spring Security处理,但遇到了一个头疼的问题:
问题现象
- 用户注册正常:通过
UserService.registerUser方法注册的用户能正常存入数据库,密码也正确加密了 - 登录时Hibernate会执行查询用户的SQL,但提交登录后直接被重定向到
localhost:8080/login?error - 控制台没有任何明确的错误或异常信息,数据库里的用户数据看起来完全正常,但就是登录失败
操作流程
- 注册用户:调用
registerUser方法,控制台输出Hibernate的查询和插入SQL,数据库确认用户已保存,密码加密 - 尝试登录:在Vaadin登录视图输入注册的账号密码,控制台输出Hibernate查询用户的SQL,但登录失败,跳转到带
error参数的登录页
相关代码
WebSecurityConfig.class
@Configuration @EnableWebSecurity public class WebSecurityConfig extends VaadinWebSecurityConfigurerAdapter { @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); setLoginView(http, Login.class); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/images/**"); super.configure(web); } }
Login.class(Vaadin视图)
@Route(value = "login") @AnonymousAllowed public class Login extends VerticalLayout implements BeforeEnterObserver { LoginForm loginForm; public Login() { loginForm = new LoginForm(); loginForm.setId("login-form"); loginForm.setAction("login"); add(loginForm); setAlignItems(Alignment.CENTER); setJustifyContentMode(JustifyContentMode.CENTER); setSizeFull(); } @Override public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { // inform the user about an authentication error if(beforeEnterEvent.getLocation() .getQueryParameters() .getParameters() .containsKey("error")) { loginForm.setError(true); } } }
UserService.class
@Service public class UserService implements UserDetailsService { private final UserRepository userRepository; @Lazy private final PasswordEncoder passwordEncoder; private final static String USER_NOT_FOUND = "User with name %s does not exist!"; @Autowired public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public void registerUser(User user) throws UsernameAlreadyExistsException { try { loadUserByUsername(user.getUsername()); throw new UsernameAlreadyExistsException(); } catch(UsernameNotFoundException e) { user.setPassword(passwordEncoder.encode(user.getPassword())); userRepository.save(user); } } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println(username); return userRepository.findByUsername(username).orElseThrow(() -> { System.out.println("Username not found " + username); return new UsernameNotFoundException(String.format(USER_NOT_FOUND, username)); }); } }
问题根源与解决方案
折腾了一整天,发完问题不到10分钟就找到了答案!
问题原因
我的User类实现了UserDetails接口,但**isAccountNonExpired()和isCredentialsNonExpired()这两个方法的默认实现返回了false**。Spring Security会检查这两个状态,只要其中一个是false,就会判定账号或凭证已过期,直接拒绝登录,而且默认情况下不会输出明显的错误日志。
排查技巧
一开始没有错误日志很难定位,直到我添加了Spring Security的调试日志配置:
logging.level.org.springframework.security=DEBUG
开启调试日志后,就能看到Spring Security判定账号过期的详细错误信息了。
解决方法
在User类中重写这两个方法,让它们返回true(如果没有特殊的过期逻辑的话):
@Override public boolean isAccountNonExpired() { return true; } @Override public boolean isCredentialsNonExpired() { return true; }
同时也要确保isAccountNonLocked()和isEnabled()方法的返回值是true(除非你有自定义的账号锁定/禁用需求)。
内容的提问来源于stack exchange,提问作者slothinflippycar
相关产品推荐
相关产品推荐

