App didático em Java 21 + Spring Boot 3.5 que mostra, do jeito mais enxuto possível, o fluxo de autenticação e autorização com Spring Security.
- Autenticação = provar quem você é (login + senha).
- Autorização = decidir o que você pode fazer (papéis e regras).
Não há banco de dados nem tela de login: os usuários ficam em memória e a API responde JSON. O objetivo é enxergar cada peça do Spring Security sem ruído.
./mvnw spring-boot:runA API sobe em http://localhost:8080. O log está em DEBUG para o pacote
org.springframework.security, então dá para acompanhar cada filtro decidindo o que fazer.
Com a API no ar, abra http://localhost:8080 no navegador. A página index.html (servida pelo
próprio Spring Boot, pasta static/) deixa escolher um usuário, chamar cada endpoint e ver o
status, o corpo da resposta e o header Authorization que foi enviado. Nada ali é protegido:
a proteção está na API que a tela chama.
O guia passo a passo com tudo pronto para copiar e colar está em docs/GUIA_CURL_POSTMAN.md. Resumo:
./scripts/testar-api.shroda todas as chamadas com curl e confere os códigos.postman/SenacSpringSecurity.postman_collection.jsonimporta no Postman com testes automáticos.docs/requisicoes.httpfunciona no VS Code (REST Client) e no IntelliJ.
| Login | Senha | Papéis |
|---|---|---|
aluno |
aluno123 |
ALUNO |
professor |
prof123 |
PROFESSOR |
admin |
admin123 |
ADMIN, PROFESSOR |
As senhas são guardadas como hash BCrypt (veja UsuarioRepository). Nunca guarde senha em texto puro.
| Endpoint | Regra | aluno | professor | admin |
|---|---|---|---|---|
GET /api/publico |
permitAll() |
✅ | ✅ | ✅ |
GET /api/perfil |
authenticated() |
✅ | ✅ | ✅ |
GET /api/aluno |
hasAnyRole(ALUNO, PROFESSOR, ADMIN) |
✅ | ✅ | ✅ |
GET /api/professor |
hasAnyRole(PROFESSOR, ADMIN) |
❌ | ✅ | ✅ |
GET /api/admin |
hasRole(ADMIN) |
❌ | ❌ | ✅ |
GET /api/notas/{aluno} |
@PreAuthorize: professor/admin, ou o próprio aluno |
só as próprias | ✅ | ✅ |
Sem login (ou com senha errada) qualquer rota protegida devolve 401 Unauthorized. Logado, mas sem o papel certo, devolve 403 Forbidden. Essa diferença é a chave de tudo: 401 é falha de autenticação, 403 é falha de autorização.
# público, sem login
curl -i http://localhost:8080/api/publico
# sem credenciais -> 401
curl -i http://localhost:8080/api/perfil
# aluno vê o próprio perfil -> 200
curl -i -u aluno:aluno123 http://localhost:8080/api/perfil
# aluno tenta a área admin -> 403
curl -i -u aluno:aluno123 http://localhost:8080/api/admin
# admin entra -> 200
curl -i -u admin:admin123 http://localhost:8080/api/admin
# aluno vê as próprias notas -> 200; as de outro -> 403
curl -i -u aluno:aluno123 http://localhost:8080/api/notas/aluno
curl -i -u aluno:aluno123 http://localhost:8080/api/notas/outrorequisição HTTP
│
▼
SecurityFilterChain (SecurityConfig)
│
├─ BasicAuthenticationFilter ── lê o header "Authorization: Basic ..."
│ │
│ ▼
│ AuthenticationManager ── DaoAuthenticationProvider
│ │ ├─ UsuarioDetailsService.loadUserByUsername(login)
│ │ └─ PasswordEncoder.matches(senhaDigitada, hashGuardado)
│ ▼
│ Authentication autenticado vai para o SecurityContextHolder
│
├─ AuthorizationFilter ── confere as regras de authorizeHttpRequests (por URL)
│
▼
Controller
└─ @PreAuthorize (por método), quando a regra depende dos dados da requisição
UsuarioRepositoryguarda os usuários (em memória, no lugar de umJpaRepository).UsuarioDetailsServiceimplementaUserDetailsService: é o ponto onde o Spring pergunta "quem é o usuárioX?" e recebe umUserDetailscom login, hash da senha e authorities.PasswordEncoder(bean emSecurityConfig) compara a senha enviada com o hash. Com BCrypt, o mesmo texto gera hashes diferentes, por isso a comparação é feita pormatches, não porequals.
- Por URL:
authorizeHttpRequestscomrequestMatchers(...). As regras valem na ordem em que são escritas; a primeira que casa com a URL decide. Por isso as mais específicas vêm antes deanyRequest(). - Por método:
@EnableMethodSecurity+@PreAuthorize("..."). Usa SpEL, então consegue olhar parâmetros do método (#aluno) e o usuário logado (authentication.name). VejaNotasController.
hasRole("ADMIN") é só açúcar para hasAuthority("ROLE_ADMIN"). User.builder().roles("ADMIN")
já acrescenta o prefixo ROLE_. Misturar os dois sem o prefixo é um erro clássico.
CSRF explora o cookie de sessão que o navegador envia sozinho. Esta API é STATELESS: não cria
sessão e cada requisição traz as credenciais no header. Sem cookie de sessão, não há o que o CSRF
explorar. Em uma aplicação com formulário de login e sessão, deixe o CSRF ligado.
Os testes ficam em src/test/java e cobrem cada linha da tabela acima:
./mvnw testAutenticacaoTest: sem credenciais, senha errada, usuário inexistente, credenciais corretas.AutorizacaoPorUrlTest: cada papel contra cada endpoint (200 ou 403).AutorizacaoPorMetodoTest: a regra@PreAuthorizede/api/notas/{aluno}.UsuarioDetailsServiceTest: teste unitário puro, sem subir o Spring, do carregamento do usuário.
- Trocar
UsuarioRepositorypor uma entidade JPA + H2. - Trocar HTTP Basic por login com formulário (
formLogin()) e observar o CSRF e a sessão. - Emitir um JWT no login e validá-lo num filtro próprio.
- Criar um papel
COORDENADORe um endpoint que só ele acessa; escrever os testes antes do código.