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

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))

其他优化建议

  1. 若不需要内存测试用户,可删除SecurityConfig中的内存认证配置,避免认证优先级冲突
  2. 给定义passwordEncoder() Bean的扩展函数文件增加@Configuration注解,确保Bean被Spring正确扫描,避免每次调用都新建Encoder实例

内容的提问来源于stack exchange,提问作者Andres Penalosa Martinell

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 19:06:07