Cadastro e Login com BCrypt em Kotlin com Spring Boot: uma jornada guiada por testes unitários e…
Introdução
Cadastro e Login com BCrypt em Kotlin com Spring Boot: uma jornada guiada por testes unitários e arquitetura limpa

Introdução
Implementar cadastro e login de usuários com senha criptografada parece trivial à primeira vista. Mas, na prática, envolve decisões arquiteturais importantes:
- Onde criptografar a senha?
- Quem é responsável pela validação?
- Como testar esse fluxo sem violar Clean Architecture?
- Como evitar que a infraestrutura “vaze” para o domínio?
Neste artigo, compartilho uma experiência real ao implementar cadastro e autenticação com BCrypt, utilizando:
- Spring Boot
- Kotlin
- Arquitetura Hexagonal (Ports & Adapters)
- Clean Code e SOLID
- Testes unitários como ferramenta central de diagnóstico
O que é o BCrypt e por que ele é importante aqui
O BCrypt é um algoritmo de hash de senhas projetado para armazenamento seguro de credenciais. Diferente de hashes tradicionais, ele é lento por design, utiliza salt automaticamente e dificulta ataques de força bruta.
Um ponto essencial — e que costuma causar confusão — é que o BCrypt nunca gera o mesmo hash duas vezes para a mesma senha. Mesmo que o valor digitado seja idêntico, o resultado do hash sempre será diferente.
Por isso, ao utilizar BCrypt, não faz sentido comparar senhas usando equals. A validação correta acontece por meio do próprio algoritmo, utilizando métodos como matches, que verificam se a senha informada corresponde ao hash armazenado, sem nunca descriptografar o valor original.
Esse detalhe, apesar de simples, é a causa de muitos problemas em fluxos de cadastro e autenticação e foi exatamente o ponto central do cenário que vou apresentar a seguir.
O problema inicial: cadastro e login não funcionavam
O cenário era simples:
- Usuário era cadastrado
- Login buscava o usuário pelo e-mail
- A senha digitada não batia, mesmo sendo “igual”
O método utilizado era o padrão:
passwordEncoder.matches(rawPassword, storedHash)
Ainda assim, o retorno era sempre false.
Nesse momento, o problema não estava claro:
- Seria configuração do BCrypt?
- Ordem errada de encode?
- Erro no banco?
- Bug de framework?
Foi aqui que os testes unitários começaram a guiar a investigação.
Primeira descoberta: o problema não era “onde” criptografar
Em um primeiro momento, pensei que o problema fosse uma falha de responsabilidade arquitetural: criptografia misturada com persistência, violação de SRP, Clean Architecture, Hexagonal Architecture, e por aí vai.
Mas essa explicação não refletia o problema real que estava acontecendo no código.
A criptografia estava no lugar certo. O BCrypt estava funcionando corretamente. O erro não era onde a senha era criptografada.
O problema estava em como o objeto era manipulado durante o fluxo.
Ao criptografar a senha, eu acabava sobrescrevendo o valor da senha no mesmo objeto de domínio. Em Kotlin, esse detalhe pode parecer inofensivo, mas acabou gerando um estado inesperado que fazia o matches retornar false no login.
Foi nesse ponto que ficou claro: o aprendizado não era sobre “camadas”, mas sobre estado, imutabilidade e clareza de fluxo.
A refatoração certa: abstrair segurança sem acoplar o domínio
Mesmo não sendo a causa do bug, havia espaço para melhorar o desenho.
A criptografia de senha faz parte da regra de negócio, mas o domínio não precisa conhecer detalhes de implementação como BCrypt ou Spring Security. Ele precisa apenas de um contrato.
Foi aí que entrou a abstração de um serviço de segurança:
interface SecurityService {
fun encode(rawPassword: String): String
}
A implementação concreta ficou na infraestrutura, usando o PasswordEncoder do Spring:
@Component
class SecurityServiceImpl(
private val passwordEncoder: PasswordEncoder
) : SecurityService {
override fun encode(rawPassword: String): String =
passwordEncoder.encode(rawPassword)
}
O domínio continua limpo. Nenhuma dependência de framework. Nenhum acoplamento desnecessário.
O caso de uso como orquestrador do fluxo
O verdadeiro divisor de águas aconteceu no caso de uso.
Foi ali que o fluxo ficou explícito, previsível e fácil de testar.
class CadastrarUsuarioUseCaseImpl(
private val dataBaseAccess: DataBaseAccess,
private val passwordEncoder: SecurityService
) : CadastrarUsuariosUseCase {
override fun cadastrar(domain: DadosUsuarioDomain): DadosUsuarioDomain {
val encodedPassword = passwordEncoder.encode(domain.senha)
val domainWithHash = domain.copy(senha = encodedPassword)
return dataBaseAccess.save(
domainWithHash.toEntity()
).toDomain()
}
}
O detalhe mais importante aqui não é o BCrypt.
É o copy.
Ao invés de mutar o objeto original, uma nova instância é criada com a senha já criptografada. Isso elimina efeitos colaterais e respeita a imutabilidade incentivada pelo Kotlin.
O que mudou de fato:
- A criptografia acontece antes da persistência
- O domínio permanece imutável
- O fluxo fica explícito e previsível
- O caso de uso passa a ser facilmente testável
- O core não depende de framework algum
Inversão de dependência na prática (sem mágica)
Para manter o core desacoplado do Spring, a configuração de beans ficou isolada na infraestrutura:
@Configuration
class BeanConfiguration {
@Bean
fun cadastroUseCaseImpl(
dataBaseAccessImpl: DataBaseAccess,
passwordEncoder: SecurityService): CadastrarUsuarioUseCaseImpl =
CadastrarUsuarioUseCaseImpl(dataBaseAccessImpl, passwordEncoder)
Nada sofisticado. Nada exagerado.
Apenas inversão de dependência aplicada de forma pragmática.
Isso garante:
- Dependency Inversion Principle na prática
- Infraestrutura plugável
- Core independente de framework
- Facilidade para testes
O papel central dos testes unitários
Esse problema só ficou realmente claro por causa dos testes.
Foram eles que deixaram explícito:
- Onde a senha estava sendo transformada
- Qual estado real chegava ao banco
- Qual valor retornava no fluxo de cadastro
- Por que o login falhava ao comparar raw vs hash
Mais do que validar comportamento, os testes forçaram clareza de design.
Eles não serviram apenas para “ver se funciona”, mas para:
- Validar responsabilidades
- Confirmar contratos entre camadas
- Guiar refatorações seguras
Esse é o tipo de situação em que testes deixam de ser um “extra” e passam a ser uma ferramenta de diagnóstico arquitetural.
Conclusão
O problema inicial não era o BCrypt.
Também não era o banco de dados. Nem o Spring. Nem a infraestrutura.
O problema estava no estado do objeto.
A solução veio quando:
- A imutabilidade foi respeitada
- O caso de uso passou a orquestrar explicitamente o fluxo
- A criptografia foi tratada de forma clara e previsível
- As dependências foram invertidas corretamente
- Os testes unitários foram usados como bússola
No fim, o maior aprendizado não foi sobre criptografia, mas sobre como pequenos detalhes de estado podem quebrar fluxos críticos — e como boas práticas, quando aplicadas com pragmatismo, ajudam a evitar exatamente esse tipo de bug.
Se quiser se aprofundar na implementação e ver o código completo, ele está disponível no meu repositório no GitHub.
메타데이터
- post_id
- 2dbdc72c0f1b
- slug
- cadastro-e-login-com-bcrypt-em-kotlin-com-spring-boot-uma-jornada-guiada-por-testes-unitários-e-2dbdc72c0f1b
- url
- https://medium.com/@proleonardopa/cadastro-e-login-com-bcrypt-em-kotlin-com-spring-boot-uma-jornada-guiada-por-testes-unit%C3%A1rios-e-2dbdc72c0f1b
- canonical_url
- https://medium.com/@proleonardopa/cadastro-e-login-com-bcrypt-em-kotlin-com-spring-boot-uma-jornada-guiada-por-testes-unit%C3%A1rios-e-2dbdc72c0f1b
- author_url
- https://medium.com/@proleonardopa
- status
- ok
- fetched_at
- 2026-07-13 06:23:13