Spring Boot 3迁移SpringSecurityConfig登录失败问题排查
Spring Boot Security配置迁移后登录失败排查
问题现象
将SecurityConfig从旧版Spring Boot迁移到新版本后,登录功能失效。页面输入明文密码,数据库存储的是encode()生成的加密密码,但无法完成登录。日志输出如下:
Hibernate: select u1_0.id,u1_0.name,u1_0.password,u1_0.username from users u1_0 where u1_0.username=? Hibernate: select a1_0.user_id,a1_0.id,a1_0.authority from authority a1_0 where a1_0.user_id=?
怀疑是id参数传递异常,但无法确定根源。
提供的代码文件
旧版配置文件
@Configuration public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder getPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService) .passwordEncoder(getPasswordEncoder()); // auth.inMemoryAuthentication() // .passwordEncoder(getPasswordEncoder()) // .withUser("trevor@craftycodr.com") // .password(getPasswordEncoder().encode("asdfasdf")) // .roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() .anyRequest().hasRole("USER").and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() .and() .logout() .logoutUrl("/logout") .permitAll(); } }
新版配置文件
@Configuration @EnableWebSecurity public class WebSecurityConfig { @Autowired private UserDetailsService userDetailsService; @Bean public UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests() .requestMatchers("/").permitAll() .anyRequest().hasRole("USER") .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() .and() .logout() .logoutUrl("/logout").permitAll() .and().build(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } }
UserDetailsServiceImpl
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepo; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepo.findByUsername(username); if(user == null) { throw new UsernameNotFoundException("Invalid username and password"); } return new CustomSecurityUser(user); } }
CustomSecurityUser
public class CustomSecurityUser extends User implements UserDetails { private static final long serialVersionUID = 464958176L; public CustomSecurityUser() {} public CustomSecurityUser(User user) { this.setAuthorities(user.getAuthorities()); this.setId(user.getId()); this.setName(user.getName()); this.setPassword(user.getPassword()); this.setUsername(user.getUsername()); } @Override public Set<Authority> getAuthorities() { return super.getAuthorities(); } @Override public String getPassword() { return super.getPassword(); } @Override public String getUsername() { return super.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
UserRepository
public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }
Authority实体
package com.freshvotes.security; import org.springframework.security.core.GrantedAuthority; import com.freshvotes.domain.User; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; @Entity public class Authority implements GrantedAuthority { private static final long serialVersionUID = -6420181486L; private Long id; private String authority; private User user; @Override public String getAuthority() { return this.authority; } public void setAuthority(String authority) { this.authority = authority; } @Id @GeneratedValue(strategy=GenerationType.IDENTITY) public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } @ManyToOne() public User getUser() { return this.user; } public void setUser(User user) { this.user = user; } }
User实体
@Entity @Table(name = "users") public class User { private Long id; private String username; private String password; private String name; private Set<Authority> authorities = new HashSet<>(); @Id @GeneratedValue(strategy=GenerationType.IDENTITY) public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="user") public Set<Authority> getAuthorities() { return this.authorities; } public void setAuthorities(Set<Authority> authorities) { this.authorities = authorities; } }
问题根源及修复方案
1. UserDetailsService实例冲突
新版配置中同时存在@Autowired注入和手动创建的UserDetailsService Bean,导致Spring容器中存在两个实例,认证逻辑混乱。
修复:删除手动创建的userDetailsService() Bean,直接使用注入的实例:
@Configuration @EnableWebSecurity public class WebSecurityConfig { @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests() .requestMatchers("/").permitAll() .anyRequest().hasRole("USER") .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() .and() .logout() .logoutUrl("/logout").permitAll() .and().build(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } }
2. 角色权限前缀不匹配
旧版hasRole("USER")会自动添加ROLE_前缀,若数据库中Authority表的authority字段存储的是USER而非ROLE_USER,会导致权限匹配失败。
修复:
- 方案一:将数据库中
authority字段值改为ROLE_USER; - 方案二:配置中改用
hasAuthority("USER")替代hasRole("USER"):
.anyRequest().hasAuthority("USER")
3. CustomSecurityUser继承风险
CustomSecurityUser继承自定义User实体可能引发字段覆盖或方法冲突,建议改用组合模式优化:
public class CustomSecurityUser implements UserDetails { private static final long serialVersionUID = 464958176L; private final User user; public CustomSecurityUser(User user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return user.getAuthorities(); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
4. 验证密码加密逻辑
确保数据库中的密码是通过BCryptPasswordEncoder.encode()生成的,可通过以下代码验证:
PasswordEncoder encoder = new BCryptPasswordEncoder(); System.out.println(encoder.encode("你的明文密码"));
将输出的加密字符串与数据库存储值对比,确认是否一致。
内容的提问来源于stack exchange,提问作者Shekel Sniffer
相关产品推荐
相关产品推荐

