💻 Exemplos: Capítulo 09: Hashing de Senhas Seguro com Passlib/Bcrypt e Segurança
Especialização em Backend com Python & FastAPI • Demonstrações Práticas Compiláveis
💻 Exemplo 1 — CryptContext com Bcrypt: Hash, Verificação e Validação de Complexidade
import re
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto", bcrypt__rounds=12)
def hash_senha(senha_plana: str) -> str:
return pwd_context.hash(senha_plana)
def verificar_senha(senha_plana: str, hash_armazenado: str) -> bool:
return pwd_context.verify(senha_plana, hash_armazenado)
def validar_complexidade(senha: str) -> tuple[bool, list[str]]:
erros: list[str] = []
if len(senha) < 8:
erros.append("mínimo de 8 caracteres")
if not re.search(r"[A-Z]", senha):
erros.append("ao menos 1 letra maiúscula")
if not re.search(r"[a-z]", senha):
erros.append("ao menos 1 letra minúscula")
if not re.search(r"[0-9]", senha):
erros.append("ao menos 1 dígito")
if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", senha):
erros.append("ao menos 1 caractere especial")
return (len(erros) == 0, erros)
valido, problemas = validar_complexidade("Tech@2026")
print("Senha válida:", valido, problemas)
hash_gerado = hash_senha("Tech@2026")
print("Hash Bcrypt:", hash_gerado[:29], "...")
print("Verificação correta:", verificar_senha("Tech@2026", hash_gerado))
print("Verificação incorreta:", verificar_senha("SenhaErrada", hash_gerado))
💻 Exemplo 2 — Autenticação Timing-Safe contra Enumeração de Usuários
from dataclasses import dataclass
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Hash "isca" pré-computado uma única vez no startup da aplicação, usado
# quando o usuário não existe, para que o tempo de resposta seja idêntico.
HASH_DUMMY = pwd_context.hash("senha-que-nunca-sera-usada-em-producao")
@dataclass
class Usuario:
email: str
senha_hash: str
BASE_USUARIOS = {
"dev@empresa.com": Usuario(email="dev@empresa.com", senha_hash=pwd_context.hash("SenhaForte#1")),
}
def autenticar(email: str, senha_tentativa: str) -> bool:
usuario = BASE_USUARIOS.get(email)
hash_alvo = usuario.senha_hash if usuario else HASH_DUMMY
# Sempre executa o verify(), exista ou não o usuário: o custo do bcrypt
# é pago igualmente nos dois casos, eliminando a diferença de tempo
# que permitiria a um atacante inferir quais e-mails estão cadastrados.
senha_confere = pwd_context.verify(senha_tentativa, hash_alvo)
return usuario is not None and senha_confere
print("Usuário existe, senha certa:", autenticar("dev@empresa.com", "SenhaForte#1"))
print("Usuário existe, senha errada:", autenticar("dev@empresa.com", "outra"))
print("Usuário inexistente:", autenticar("fantasma@empresa.com", "qualquer"))
💻 Exemplo 3 — Migração Transparente de Hashes Legados com needs_rehash
from passlib.context import CryptContext
# "bcrypt" é o esquema ativo; "md5_crypt" só é aceito para leitura de
# hashes legados e é automaticamente marcado como depreciado.
pwd_context = CryptContext(
schemes=["bcrypt", "md5_crypt"],
deprecated=["md5_crypt"],
bcrypt__rounds=12,
)
class RepositorioUsuariosFake:
def __init__(self) -> None:
# Simula um usuário legado migrado de um sistema antigo em MD5.
self._hash_legado = pwd_context.hash("senha123", scheme="md5_crypt")
def buscar_hash(self) -> str:
return self._hash_legado
def atualizar_hash(self, novo_hash: str) -> None:
self._hash_legado = novo_hash
print("Hash do usuário atualizado no banco para o esquema atual.")
def login(senha_tentativa: str, repo: RepositorioUsuariosFake) -> str:
hash_atual = repo.buscar_hash()
if not pwd_context.verify(senha_tentativa, hash_atual):
raise ValueError("Credenciais inválidas")
if pwd_context.needs_rehash(hash_atual):
novo_hash = pwd_context.hash(senha_tentativa)
repo.atualizar_hash(novo_hash)
return "autenticado"
repositorio = RepositorioUsuariosFake()
print("Hash antes do login:", repositorio.buscar_hash()[:10], "...")
print("Resultado do login:", login("senha123", repositorio))
print("Hash depois do login:", repositorio.buscar_hash()[:10], "...")