🗺️ Mapa Arquitetural do Tópico

flowchart LR
    BROWSER["🌐 Navegador"] -->|"1. POST /login (credenciais)"| API["🏛️ Backend Auth Service"]
    API -->|"2. Retorna Access Token (Memória) + Refresh Token (Cookie HTTP-Only)"| BROWSER
    BROWSER -->|"3. Requisição Protegida (Header Bearer JWT)"| API
    API -->|"4. Valida Roles (RBAC: ADMIN / USER)"| DB["PostgreSQL"]
    
    style BROWSER fill:#e1f5fe,stroke:#03a9f4,stroke-width:1px
    style API fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style DB fill:#fff3e0,stroke:#ff9800,stroke-width:1px

🧩 1. O problema: HTTP não lembra de ninguém

Cada requisição HTTP chega ao servidor sem saber das anteriores. Para o servidor “lembrar” quem fez login, a aplicação precisa mandar uma prova de identidade em toda requisição. Existem duas famílias de solução:

  Sessão no servidor Token (JWT)
O que o cliente guarda Só um ID aleatório (JSESSIONID) num cookie O próprio token, com os dados do usuário dentro
O que o servidor guarda A sessão inteira, em memória ou no Redis Nada: ele só confere a assinatura
Logout imediato Fácil: apaga a sessão Difícil: o token vale até expirar
Vários servidores / microsserviços Precisam compartilhar o armazenamento de sessões Qualquer serviço que tenha a chave valida sozinho
Clientes mobile e de outros domínios Cookies dão trabalho Header Authorization funciona em qualquer cliente

A sessão continua sendo uma ótima escolha para uma aplicação web tradicional num só domínio. O JWT ganha quando há vários clientes (web, mobile, desktop) e vários serviços, que é o cenário típico deste curso. Pense na série Lista de Tarefas: uma API e nove clientes diferentes.

🔑 2. Anatomia de um JWT

Um JWT (JSON Web Token) são três partes em Base64URL separadas por ponto: cabeçalho.payload.assinatura.

  • Cabeçalho: o algoritmo de assinatura, por exemplo {"alg":"HS256"}.
  • Payload: as claims, que são as afirmações sobre o usuário. As padronizadas são sub (quem é), iat (emitido em), exp (expira em). As da aplicação, como roles, também vão aqui.
  • Assinatura: um HMAC (HS256, com chave secreta) ou uma assinatura RSA/ECDSA (RS256/ES256, com par de chaves) calculada sobre as duas primeiras partes.

O ponto mais mal entendido: um JWT não é criptografado, só assinado. Qualquer pessoa lê o payload:

// Um JWT não é criptografado: qualquer um lê o payload. Só a assinatura impede a alteração.
export function lerPayload(token: string): Record<string, unknown> {
  const [, payload] = token.split(".");
  if (!payload) throw new Error("token malformado");
  return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
}
// lerPayload(token) → { sub: 'ana', roles: [ 'ADMIN' ], exp: 1767225600 }

Por isso nunca coloque no payload nada que não possa ser lido: senha, CPF, dados de cartão. O que a assinatura garante é que ninguém altera o payload. Se o atacante trocar "roles": ["USER"] por ["ADMIN"], a assinatura deixa de bater e o servidor rejeita o token.

HS256 ou RS256? Com HS256, a mesma chave assina e verifica, então todo serviço que verifica também consegue emitir tokens. Com RS256, só o serviço de autenticação tem a chave privada; os outros recebem a pública e só verificam. Para um único backend, HS256 basta. Com vários serviços, prefira RS256.

🔄 3. Access token e refresh token

Um token que vale muito tempo é perigoso: se vazar, o atacante tem acesso até ele expirar. Um token que vale pouco obriga o usuário a logar toda hora. A solução é usar dois tokens:

  Access token Refresh token
Vida útil Curta (5 a 15 minutos) Longa (dias)
Para que serve Autorizar cada requisição à API Pedir um novo access token
Onde fica no navegador Em memória (uma variável do JavaScript) Cookie HttpOnly
Vai em Header Authorization: Bearer ... Só para a rota /api/auth/refresh

O cookie do refresh token é montado com quatro atributos, cada um contra um ataque:

// Refresh token em cookie: o JavaScript da página não consegue lê-lo (HttpOnly)
export function cookieRefresh(token: string, diasValidade: number): string {
  return [
    `refresh_token=${token}`,
    "HttpOnly",                 // invisível para document.cookie → protege contra XSS
    "Secure",                   // só trafega em HTTPS
    "SameSite=Strict",          // não é enviado em requisições de outros sites → mitiga CSRF
    "Path=/api/auth/refresh",   // só vai para a rota que renova o access token
    `Max-Age=${diasValidade * 24 * 60 * 60}`,
  ].join("; ");
}
// → refresh_token=abc123; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; Max-Age=604800

Ao recarregar a página, o access token some da memória. O frontend chama /api/auth/refresh, o navegador envia o cookie sozinho, e a API devolve um access token novo. Com rotação, cada uso do refresh token gera também um refresh token novo e invalida o anterior. Assim, um refresh token roubado e reaproveitado é detectado.

🛡️ 4. Validando no backend e autorizando por papel (RBAC)

No Spring Boot, a validação fica num filtro que roda antes de qualquer controller:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);        // sem token: segue como anônimo
            return;
        }
        String jwt = header.substring(7);
        String usuario = tokenProvider.extractUsername(jwt);   // também verifica a assinatura
        if (usuario != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails detalhes = userDetailsService.loadUserByUsername(usuario);
            if (tokenProvider.isTokenValid(jwt, detalhes)) {   // usuário bate e não expirou
                var auth = new UsernamePasswordAuthenticationToken(detalhes, null, detalhes.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }
}

Autenticar responde “quem é você?”. Autorizar responde “o que você pode fazer?”. O RBAC (Role-Based Access Control) responde à segunda pergunta por papéis:

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")      // só quem tem a authority ROLE_ADMIN
public ResponseEntity<Void> excluir(@PathVariable Long id) { ... }

A configuração diz o que é público e deixa o restante protegido:

http.csrf(AbstractHttpConfigurer::disable)            // ok: a API não usa cookie de sessão
    .authorizeHttpRequests(req -> req
        .requestMatchers("/api/auth/**", "/swagger-ui/**").permitAll()
        .anyRequest().authenticated())
    .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

No frontend, esconder o botão “Excluir” de quem não é administrador melhora a experiência, mas não é segurança. A regra que vale é a do backend: qualquer um pode chamar a API direto, sem passar pela tela.

⚖️ 5. Comparação: onde guardar o token no navegador

Local XSS (script injetado lê o token?) CSRF (outro site usa o token?) Sobrevive a recarregar a página?
localStorage ❌ Sim, lê ✅ Não (não vai sozinho) ✅ Sim
sessionStorage ❌ Sim, lê ✅ Não Só na mesma aba
Memória (variável JS) 🟡 Mais difícil (some ao recarregar) ✅ Não ❌ Não, por isso existe o refresh
Cookie HttpOnly + SameSite ✅ Não, o JS não lê 🟡 Mitigado pelo SameSite ✅ Sim

A combinação recomendada, access em memória + refresh em cookie HttpOnly, junta o melhor das duas últimas linhas. O localStorage é o mais usado em tutoriais por ser simples. Funciona, mas qualquer falha de XSS expõe o token.

⚠️ 6. Armadilhas comuns

Sintoma ou risco Causa Correção
Token forjado aceito Biblioteca configurada para aceitar alg: none, ou o algoritmo vem do próprio token Fixe o algoritmo esperado na validação
Qualquer pessoa com o código-fonte emite tokens jwt.secret escrito no application.properties versionado Segredo em variável de ambiente ou cofre (Mod 12, cap. 07)
Usuário demitido continua acessando Token de vida longa e sem revogação Access curto + refresh com rotação e lista de revogação
hasRole('ADMIN') nega acesso ao administrador A authority foi salva como ADMIN, e o hasRole procura ROLE_ADMIN Salve com o prefixo ROLE_ ou use hasAuthority('ADMIN')
Dados sensíveis expostos CPF ou e-mail no payload “porque é prático” Payload só com sub, papéis e datas
API aceita token de outro sistema Mesma chave em vários ambientes, sem checar iss e aud Chave por ambiente; valide emissor e público

🗂️ 7. No Catálogo de Projetos

  • fullstack_estoque_02_spring_auth implementa o fluxo deste capítulo em Spring Boot. Arquivos para estudar, em gestaodeestoques/src/main/java/br/com/aula/gestaodeestoques/:
    • config/security/JwtTokenProvider.java: gera e valida o token com HS256 e expiração de 1 hora (jwt.expiration-in-ms=3600000).
    • config/security/JwtAuthenticationFilter.java: o filtro da Seção 4, quase linha a linha.
    • config/SecurityConfig.java: API STATELESS, CSRF desligado e a lista de rotas públicas.
    • controller/CategoriaController.java: @PreAuthorize("hasRole('ADMIN')") nas operações de escrita. Os papéis são criados como ROLE_ADMIN e ROLE_USER em config/DataSeeder.java, o jeito certo para o hasRole.
  • Para analisar criticamente no mesmo projeto:
    • o frontend (resources/static/js/auth.js) guarda o token em localStorage (veja a Seção 5);
    • não há refresh token: depois de 1 hora, o usuário loga de novo;
    • o jwt.secret está no application.properties versionado.

    Corrigir as três é um bom exercício extra: mover o access token para a memória, criar a rota /api/auth/refresh com o refresh token em cookie HttpOnly e ler o segredo de uma variável de ambiente.

  • fullstack_liston_01_spring_angular traz a mesma ideia com um cliente Angular e Ionic (backend/.../config/SecurityConfig.java).