Spring Boot Security凭据正确却返回Bad Credentials问题排查
登录返回Bad Credentials问题排查与解决
问题场景
基于Spring Boot + MySQL的应用,通过POST /api/users 接口添加新用户时,加密后的密码已成功存入数据库,但使用正确凭据登录表单时,返回Bad Credentials错误。
相关代码
SecurityConfig.java
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public UserDetailsService userDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); } @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests() // .requestMatchers("/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/users").permitAll() .and().formLogin().permitAll() .and().logout().permitAll() .and().cors().and().csrf().disable() .httpBasic(); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
UserController.java
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; @PostMapping() public User insertUser(@RequestBody User user) { if (user.getId() != 0) { return null; } user.setPassword(passwordEncoder.encode(user.getPassword())); return userRepository.save(user); } }
User.java(实体类)
@Entity @Table(name = "users") public class User { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @NotEmpty @Column(name = "username",nullable = false,unique = true) private String username; @NotEmpty @Column(name = "password",nullable = false) private String password; private boolean enabled = true; @Column(name = "email") @Email(message = "Email should be valid") private String email; @Column(name = "address") private String address; @Column(name = "AFM") private Long AFM; @Column(name = "phone_number") private Long phone_number; public User() { } public User(String name, String email, String address, Long AFM, Long phone_number) { this.username = name; this.email = email; this.address = address; this.AFM = AFM; this.phone_number = phone_number; } //Getters Setters }
创建用户的请求体
{ "username":"nikos gourn", "email":"nikos@gmail.com", "address":"kapio allo meros", "afm":98776, "phone_number":2109017131, "password":"123" }
错误原因分析
1. JdbcUserDetailsManager默认表结构不兼容
你使用的JdbcUserDetailsManager是Spring Security默认的JDBC实现,它强制依赖特定的表结构:
- 必须存在
authorities表存储用户权限/角色,否则无法加载完整的用户认证信息 - 即使
users表包含username、password、enabled字段,缺少权限表会导致认证逻辑异常
2. 密码编码器未关联到认证管理器
虽然你定义了BCryptPasswordEncoder Bean,但JdbcUserDetailsManager没有自动关联该编码器,导致它无法用BCrypt算法验证密码。
解决方案
方案一:适配Spring Security默认表结构
- 创建
authorities表:
CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) );
- 添加用户时同步插入权限记录(例如给用户默认
ROLE_USER权限):
在UserController的insertUser方法中,新增插入权限的逻辑(可通过JdbcTemplate或直接调用JdbcUserDetailsManager的方法实现)。 - 给
JdbcUserDetailsManager绑定密码编码器:
修改SecurityConfig中的Bean定义:
@Bean public UserDetailsService userDetailsService(DataSource dataSource, PasswordEncoder passwordEncoder) { JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource); manager.setPasswordEncoder(passwordEncoder); return manager; }
方案二:自定义UserDetailsService(更灵活)
如果不想使用默认表结构,自定义实现UserDetailsService来适配你的users表:
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); // 自定义用户权限,示例为默认ROLE_USER return User.withUsername(user.getUsername()) .password(user.getPassword()) .roles("USER") .enabled(user.isEnabled()) .build(); } }
然后修改SecurityConfig中的UserDetailsService Bean:
@Bean public UserDetailsService userDetailsService(CustomUserDetailsService customUserDetailsService) { return customUserDetailsService; }
调试流程
- 验证数据库数据:确认
users表中的password字段是BCrypt加密后的字符串(以$2a$开头),而非明文。 - 开启调试日志:在
application.properties中添加配置,查看认证全流程日志:
logging.level.org.springframework.security=DEBUG
- 断点调试:在
loadUserByUsername方法和BCryptPasswordEncoder.matches方法打代码断点,检查用户是否被正确查询、密码匹配逻辑是否正常执行。 - 手动验证密码匹配:编写测试代码调用
passwordEncoder.matches("123", 数据库中的加密密码),确认返回值为true。
内容的提问来源于stack exchange,提问作者Ledrake
相关产品推荐
相关产品推荐

