Spring Security BCryptPasswordEncoder相同密码提示非BCrypt格式错误求解
登录接口BCrypt密码校验错误问题
使用Kotlin + Spring Boot 2.5.5 + MySQL开发登录服务,用户注册、删除、更新功能均正常,仅在通过Postman调用登录接口时收到如下错误:
2021-10-26 18:16:28.558 WARN 26076 --- [nio-8080-exec-2] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt
已检索Stack Overflow同类问题,现有解决方案均不适用,部分与业务场景无关。
1. SecurityConfig类代码
@Configuration @EnableWebSecurity class SecurityConfig(private val dataSource: DataSource) : WebSecurityConfigurerAdapter() { override fun configure(auth: AuthenticationManagerBuilder) { auth.inMemoryAuthentication() .withUser("user").password("password") .authorities("USER", "ADMIN", "CONTRIBUTOR") } override fun configure(http: HttpSecurity?) { http!!.csrf().disable() .authorizeRequests() .antMatchers("/admin/**").hasAuthority("ADMIN") .antMatchers("/contributor/**").hasAuthority("CONTRIBUTOR") .antMatchers("/anonymous*").anonymous() .antMatchers("/login/**").permitAll() .antMatchers("/login*").permitAll() .antMatchers("/**").hasAuthority("USER") .anyRequest().authenticated() .and() .formLogin().permitAll() .and() .logout().permitAll() } @Autowired fun configureGlobal(auth: AuthenticationManagerBuilder) { auth.jdbcAuthentication() .passwordEncoder(passwordEncoder()) .dataSource(dataSource) .usersByUsernameQuery("select username,encrypted_password,'true' from dragonline.user where username=?") .authoritiesByUsernameQuery("select user_username,roles_name from dragonline.user_has_roles where user_username=?") } } class AppInitializer : WebApplicationInitializer { override fun onStartup(servletContext: ServletContext) { val root = AnnotationConfigWebApplicationContext() root.register(SecurityConfig::class.java) servletContext.addListener(ContextLoaderListener(root)) servletContext.addFilter("securityFilter", DelegatingFilterProxy("springSecurityFilterChain")) .addMappingForUrlPatterns(null, false, "/*") } }
2. 登录控制器代码
@RestController @RequestMapping(path = ["/login"]) class Login(private val userRegistrationService: UserRegistrationService) { @PostMapping(path = ["/register"], consumes = [MediaType.APPLICATION_JSON_VALUE]) fun register(@RequestBody user: UserRegistrationDTO, response: HttpServletResponse): UserRegistrationDTO { user.confirmPassword() val domainUser = user.toDomain() userRegistrationService.saveUser(domainUser) response.status = HttpServletResponse.SC_CREATED return user.maskPassword() } @CrossOrigin @PostMapping(path = ["/find-user"], consumes = [MediaType.APPLICATION_JSON_VALUE]) fun login(@RequestBody user: UserLoginDTO, response: HttpServletResponse): UserLoginDTO { val userFound = userRegistrationService.findUser(user.username) println("The password stored ind DB is ${userFound?.encryptedPassword}") if (passwordEncoder().matches(userFound?.encryptedPassword, user.password)) { response.status = HttpServletResponse.SC_OK response.setHeader( "Session-ID", "${passwordEncoder().encode(user.username)}@.@${passwordEncoder().encode(user.password)}" ) } else { response.status = HttpServletResponse.SC_NOT_FOUND } return user.maskPassword() } @GetMapping(path = ["/delete-user/{userId}"]) fun deleteUser(@PathVariable userId: String) { userRegistrationService.deleteUser(userId) } }
3. 数据库对应记录
字段依次为username、encrypted_password、email、name、lastnames、session_id、session_active:
'test', '$2a$10$Z/FSxELmMtV2zpgFVYzDi.dprUybnzJxF6f/kZan7DqHfQ9VDjmQq', 'test@test.com', 'Tester', 'Testing a Test', NULL, '0'
4. UserService代码
@Service @Transactional class UserRegistrationService( private val userRegistrationRepository: UserRegistrationRepository ) { fun findUser(userId: String) = userRegistrationRepository.findByUsername(userId) ?: userRegistrationRepository.findByEmail(userId) fun saveUser(user: UserRegistrationData) = userRegistrationRepository.save(user.toPersistence()) fun deleteUser(userId: String) { val user = userRegistrationRepository.findByUsername(userId) ?: userRegistrationRepository.findByEmail(userId) ?: throw Exception("The user can't be deleted, because it doesn't exist") user.username.let { userRegistrationRepository.deleteByUsername(it) } } }
5. Repository代码
@Repository interface UserRegistrationRepository: JpaRepository<User, String> { fun findByUsername(username: String): User? fun findByEmail(email: String): User? fun deleteByUsername(username: String): Long fun deleteByEmail(email: String): Long }
6. 扩展函数文件代码(含Encoder Bean定义)
fun UserRegistrationData.toPersistence() = User( username = this.username, encryptedPassword = this.encryptedPassword, email = this.email, name = this.name, lastnames = this.lastnames, roles = this.roles.map { it.toPersistence() }.toHashSet() ) fun UserRole.toPersistence() = Role( name = this.name ) fun UserLoginDTO.maskPassword() = UserLoginDTO( username = this.username, password = "***********" ) fun UserRegistrationDTO.confirmPassword() { if (this.password != this.confirmPassword) throw ResponseStatusException( HttpStatus.BAD_REQUEST, "The passwords don't match" ) } fun UserRegistrationDTO.maskPassword() = UserRegistrationDTO( username = this.username, password = "***********", confirmPassword = "***********", email = this.email, name = this.name, lastnames = this.lastnames, admin = this.admin, contributor = this.contributor ) fun UserRegistrationDTO.toDomain(): UserRegistrationData { val user = UserRegistrationData( name = this.name, lastnames = this.lastnames, email = this.email, username = this.username, encryptedPassword = passwordEncoder().encode(this.password), roles = mutableListOf(UserRole.USER) ) if(this.admin) user.roles.add(UserRole.ADMIN) if(this.contributor) user.roles.add(UserRole.CONTRIBUTOR) return user } @Bean fun passwordEncoder(): PasswordEncoder { return BCryptPasswordEncoder() }
7. 模型类代码
data class UserRegistrationDTO( val name: String? = null, val lastnames: String? = null, @field:NotBlank val username: String, @field:Email val email: String, @field:NotBlank val password: String, @field:NotBlank val confirmPassword: String, val admin: Boolean = false, val contributor: Boolean = false ) data class UserLoginDTO( val username: String, val password: String ) data class UserRegistrationData( val name: String? = null, val lastnames: String? = null, @field:NotBlank val username: String, @field:Email val email: String, @field:NotBlank val encryptedPassword: String, val roles: MutableList<UserRole> = mutableListOf(UserRole.USER) ) class User( @Id var username: String, @Column var email: String, @Column var name: String?, @Column var lastnames: String?, @Column var encryptedPassword: String, @OneToMany @JoinTable( name = "user_has_roles", joinColumns = [JoinColumn(name = "user_username")], inverseJoinColumns = [JoinColumn(name = "roles_name")] ) var roles: Set<Role>? = null )
解决方案
核心错误原因
BCryptPasswordEncoder.matches()方法的参数顺序颠倒,这是触发报错的直接原因:
- 该方法的正确签名为
matches(CharSequence rawPassword, String encodedPassword)- 第一个参数:用户输入的明文密码
- 第二个参数:数据库中存储的已加密BCrypt格式密码
- 现有代码将加密后的密码作为第一个参数传入,BCrypt解析该参数不符合明文特征,因此抛出对应报错。
修复代码
将登录接口中的校验逻辑修改为:
if (userFound != null && passwordEncoder().matches(user.password, userFound.encryptedPassword))
其他优化建议
- 若不需要内存测试用户,可删除
SecurityConfig中的内存认证配置,避免认证优先级冲突 - 给定义
passwordEncoder()Bean的扩展函数文件增加@Configuration注解,确保Bean被Spring正确扫描,避免每次调用都新建Encoder实例
内容的提问来源于stack exchange,提问作者Andres Penalosa Martinell
相关产品推荐
相关产品推荐

