← Back to list

Cadastro e Login com BCrypt em Kotlin com Spring Boot: uma jornada guiada por testes unitários e…

Introdução

Leonardo Paulino · 2026-01-30 10:50 · 0 claps · 4.0 min read
#kotlin #spring-boot #spring-security #kotlin-spring #bcrypt
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

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