📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Java e Spring Boot. Recomendado revisar antes de começar.

🔑 Login de Usuários 04 — Segurança em Produção (Processo)

Nota: assim como no _03, o código-fonte real vive em cadastrousuarios04/ (nome do módulo Maven).

Trilha de Aprendizado — Projeto 4 de 4

🎓 Nível profissional simulado: Sênior. Você não recebe uma lista de features prontas: recebe uma reclamação real de um gestor de TI, extrai requisitos você mesmo, prioriza um backlog, e entrega através de branches + PRs + code review — igual ao Controle de Gastos 04, mas aplicado ao domínio de autenticação, onde as decisões técnicas erradas viram incidente de segurança, não só bug.`

🎯 Objetivo

Partindo do código do Login de Usuários 03 (RBAC + painel admin), evoluir o sistema com 3 capacidades que todo sistema de autenticação real precisa e que a trilha nunca cobriu: recuperação de senha, bloqueio de conta contra força bruta, e auditoria de acessos.

—`

🧑‍💼 Fase 1 — Levantamento de Requisitos

O Briefing do Gestor de TI

Carlos é o responsável pela área de TI de uma empresa de médio porte que usa o sistema de cadastro de usuários internamente. Ele manda o seguinte relato, depois de um incidente:

“Precisamos conversar sobre o sistema de login. Primeiro, todo mês eu recebo pelo menos 5 chamados de gente que esqueceu a senha e eu tenho que resetar manualmente no banco — isso não devia depender de mim. Segundo — e esse é sério — semana passada percebi no log do servidor que alguém tentou logar na conta do financeiro umas 200 vezes seguidas em poucos minutos, óbvio que era um ataque de força bruta tentando adivinhar a senha. O sistema não bloqueou nada, não me avisou de nada, eu só vi porque fui olhar o log por acaso. Terceiro, no mês passado tivemos uma auditoria externa e pediram um relatório de ‘quem acessou o sistema e quando’ dos últimos 3 meses — a gente simplesmente não tem esse dado, tive que inventar uma desculpa. Isso é o tipo de coisa que se acontecer de novo pode gerar multa.”

Extraindo Requisitos do Briefing

Requisitos Funcionais (RF):

ID Requisito Origem no briefing
RF01 O sistema deve permitir que o próprio usuário redefina sua senha via um link temporário, sem depender do TI “todo mês eu recebo pelo menos 5 chamados… isso não devia depender de mim”
RF02 O sistema deve bloquear temporariamente uma conta após um número de tentativas de login malsucedidas seguidas “alguém tentou logar… umas 200 vezes seguidas… o sistema não bloqueou nada”
RF03 O sistema deve notificar/registrar tentativas suspeitas de login em tempo hábil, não só em log de servidor “eu só vi porque fui olhar o log por acaso”
RF04 O sistema deve manter um registro auditável (quem, quando, sucesso ou falha) de todos os acessos, consultável por período “pediram um relatório de ‘quem acessou e quando’… a gente não tem esse dado”

Requisitos Não-Funcionais (RNF):

ID Requisito Origem no briefing
RNF01 O link de redefinição de senha deve expirar e ser de uso único implícito — um link de reset que nunca expira é uma falha de segurança, mesmo não mencionada explicitamente
RNF02 O registro de auditoria não pode ser apagável por usuários comuns implícito — dado de compliance precisa ser imutável para servir como prova em auditoria

💡 Nota sênior: RNF01 e RNF02 não estão escritos no briefing — Carlos não é um especialista em segurança, ele descreve sintomas. Preencher esses requisitos “não-ditos” a partir do domínio é justamente o trabalho de quem já tem experiência — um júnior implementaria só o que foi pedido literalmente e entregaria um reset de senha com link eterno.

Das Requisitos às User Stories

Requisito User Story
RF01 + RNF01 US05 — Recuperação de senha por link temporário e de uso único
RF02 US06 — Bloqueio de conta após tentativas de login malsucedidas
RF03 + RF04 + RNF02 US07 — Auditoria imutável de tentativas de acesso

—`

📋 Fase 2 — Backlog do Produto e Sprint Planning

Product Backlog

ID User Story Prioridade
US05 Como usuário, quero redefinir minha senha sozinho via link temporário, sem depender do TI Alta
US06 Como gestor de TI, quero que contas sejam bloqueadas temporariamente após tentativas de login seguidas malsucedidas, para mitigar força bruta Alta
US07 Como gestor de TI, quero um relatório auditável de acessos (sucesso/falha, data, usuário), para atender auditorias externas Alta

Sprint Planning

Quadro Sprint (Kanban)

User Story To Do Doing Done
US06 — Bloqueio de conta      
US07 — Auditoria de acessos      
US05 — Recuperação de senha      

—`

🌿 Fase 3 — Fluxo de Trabalho em Equipe (Git Flow)

Mesmo fluxo do Controle de Gastos 04: uma branch feature/US0Xx-nome-curto por User Story, commit semântico, PR com o template abaixo, checklist de code review (você mesmo simulando os dois papéis), squash merge.

git checkout main && git pull origin main
git checkout -b feature/US06-bloqueio-conta
# ... implementa, commita, push ...
git push -u origin feature/US06-bloqueio-conta
# abre PR no GitHub, revisa com o checklist, squash merge
git checkout main && git pull origin main && git branch -d feature/US06-bloqueio-conta

Template de Pull Request

## O que mudou`

## User Story
US0Xx — <descrição>`

## Como testar
1.
2.`

## Checklist
- [ ] `./mvnw test` passa
- [ ] Testei o fluxo feliz e um caso de borda de segurança (ex.: token expirado, conta já bloqueada)
- [ ] Se tomei uma decisão técnica relevante, registrei um ADR

—`

🛠️ Fase 4 — Implementação das User Stories

US06 — Bloqueio de Conta após Tentativas Malsucedidas

1. Novos campos em User:

private int tentativasFalhas = 0;
private LocalDateTime bloqueadoAte;

2. LoginSecurityService — lógica pura, testável sem Spring Security:

package br.com.cadastro.usuarios.service;

import br.com.cadastro.usuarios.entity.User;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
public class LoginSecurityService {

    private static final int MAX_TENTATIVAS = 5;
    private static final int MINUTOS_BLOQUEIO = 15;

    public boolean estaBloqueado(User user) {
        return user.getBloqueadoAte() != null && user.getBloqueadoAte().isAfter(LocalDateTime.now());
    }

    public void registrarFalha(User user) {
        user.setTentativasFalhas(user.getTentativasFalhas() + 1);
        if (user.getTentativasFalhas() >= MAX_TENTATIVAS) {
            user.setBloqueadoAte(LocalDateTime.now().plusMinutes(MINUTOS_BLOQUEIO));
        }
    }

    public void registrarSucesso(User user) {
        user.setTentativasFalhas(0);
        user.setBloqueadoAte(null);
    }
}

3. Conectar ao Spring Security via AuthenticationFailureHandler/AuthenticationSuccessHandler customizados no SecurityConfig, chamando registrarFalha/registrarSucesso e persistindo com UserRepository.save(user). O UserDetailsService (AuthorizationService) também precisa checar estaBloqueado(user) e lançar LockedException antes de validar a senha.

💡 Por que bloqueio temporário (15 min) e não permanente? Bloqueio permanente até intervenção manual do admin é a resposta mais segura, mas também vira um vetor de negação de serviço: um atacante que só sabe o e-mail de alguém pode bloquear a conta da vítima de propósito, digitando senha errada de propósito. Backoff temporário mitiga força bruta sem abrir essa porta. Isso vira o ADR 004.

US07 — Auditoria Imutável de Acessos

1. Nova entidade LogAcesso (tabela própria, nunca reaproveitando users):

@Entity
@Table(name = "log_acessos")
public class LogAcesso {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private boolean sucesso;
    private LocalDateTime dataHora = LocalDateTime.now();
    // Sem update/delete expostos em nenhum controller — só INSERT e SELECT.
}

Populada pelos mesmos handlers de sucesso/falha da US06. O UserController ganha GET /auditoria (protegido, só ADMIN) listando os registros — reaproveitando o padrão já usado em /relatorios no _03.

1. Nova entidade PasswordResetToken:

@Entity
public class PasswordResetToken {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String token; // UUID
    @ManyToOne
    private User user;
    private LocalDateTime expiraEm;
    private boolean usado = false;
}

2. PasswordResetService — lógica pura, testável:

@Service
public class PasswordResetService {

    public PasswordResetToken gerarToken(User user) {
        PasswordResetToken token = new PasswordResetToken();
        token.setUser(user);
        token.setToken(UUID.randomUUID().toString());
        token.setExpiraEm(LocalDateTime.now().plusMinutes(30));
        return token;
    }

    public boolean tokenValido(PasswordResetToken token) {
        return !token.isUsado() && token.getExpiraEm().isAfter(LocalDateTime.now());
    }
}

3. Fluxo web: GET /esqueci-senha (form de e-mail) → gera e “envia” o link (para fins didáticos, exibido na tela/console em vez de e-mail real — deixar isso explícito nos comentários do código) → GET /redefinir-senha?token=xxx valida tokenValido() antes de mostrar o formulário de nova senha → POST atualiza a senha e marca usado = true.

⚠️ Cuidado de segurança didático: a tela de “esqueci minha senha” nunca deve revelar se o e-mail existe ou não no sistema (ex.: “e-mail não encontrado” vs “link enviado”). Sempre responda com a mesma mensagem genérica (“se o e-mail existir, um link foi enviado”) — do contrário, o formulário vira uma ferramenta para descobrir quais e-mails têm conta no sistema.

—`

🧭 Decisões Técnicas (ADRs)

ADR 004 — Bloqueio temporário (15 min / 5 tentativas), não permanente

ADR 005 — LogAcesso em tabela própria, sem UPDATE/DELETE expostos

ADR 006 — Token de reset de uso único com expiração de 30 minutos

—`

🧪 Testes Automatizados

class LoginSecurityServiceTest {

    private final LoginSecurityService service = new LoginSecurityService();

    @Test
    void registrarFalha_deveBloquearApos5Tentativas() {
        User user = new User();
        for (int i = 0; i < 5; i++) service.registrarFalha(user);

        assertTrue(service.estaBloqueado(user));
    }

    @Test
    void registrarFalha_naoDeveBloquearAntesDoLimite() {
        User user = new User();
        for (int i = 0; i < 4; i++) service.registrarFalha(user);

        assertFalse(service.estaBloqueado(user));
    }

    @Test
    void registrarSucesso_deveZerarTentativasEBloqueio() {
        User user = new User();
        for (int i = 0; i < 5; i++) service.registrarFalha(user);

        service.registrarSucesso(user);

        assertFalse(service.estaBloqueado(user));
        assertEquals(0, user.getTentativasFalhas());
    }
}

class PasswordResetServiceTest {

    private final PasswordResetService service = new PasswordResetService();

    @Test
    void tokenValido_deveSerFalsoQuandoJaUsado() {
        PasswordResetToken token = service.gerarToken(new User());
        token.setUsado(true);

        assertFalse(service.tokenValido(token));
    }

    @Test
    void tokenValido_deveSerFalsoQuandoExpirado() {
        PasswordResetToken token = service.gerarToken(new User());
        token.setExpiraEm(LocalDateTime.now().minusMinutes(1));

        assertFalse(service.tokenValido(token));
    }
}

💡 Por que testar o caso “4 tentativas não bloqueia”? Assim como o orcamentoEstourado(2000, 2000) == false do Controle de Gastos 04, esse é um teste de limite exato (boundary test) — garante que o bloqueio acontece na 5ª tentativa, não na 4ª nem na 6ª. Regras de segurança com “off-by-one” (erro de um a mais/a menos) são um dos bugs mais comuns e mais perigosos nessa categoria de código.


—`

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd javaweb_login_04_adrs

2. Execute a aplicação e os testes

./mvnw spring-boot:run
./mvnw test

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/javaweb_login_04_adrs`

✅ Checkpoint Final

  1. As 3 User Stories (US05, US06, US07) estão em branches próprias, PRs mescladas via squash.
  2. ./mvnw test passa, cobrindo LoginSecurityService e PasswordResetService.
  3. Testou manualmente: errar a senha 5 vezes seguidas e confirmar o bloqueio; solicitar reset de senha, usar o link, tentar reusá-lo (deve falhar); acessar /auditoria como ADMIN e ver os registros de sucesso/falha.
  4. As 3 decisões técnicas (ADR 004-006) estão registradas em docs/adr/.`

🏆 Conclusão da Trilha (4 de 4)

Projeto Nível simulado Foco  
01 Estagiário Login/cadastro guiado, role única via enum  
02 Júnior Refatoração SOLID/DRY — Service layer, DTOs, Bean Validation  
03 Pleno RBAC muitos-para-muitos, painel admin, relatórios  
04 Sênior Processo: requisitos, backlog, Git Flow, testes, ADRs — segurança em produção `

📋 Resumo de Comandos

# Fluxo de branch por User Story
git checkout main && git pull origin main
git checkout -b feature/US0Xx-nome-curto
git add . && git commit -m "feat: descricao (US0Xx)"
git push -u origin feature/US0Xx-nome-curto

# Apos a PR ser mesclada (squash)
git checkout main && git pull origin main && git branch -d feature/US0Xx-nome-curto

# Rodar os testes antes de cada push
./mvnw test

# Rodar localmente
cd cadastrousuarios04
./mvnw spring-boot:run

Voltar para Projetos