Capítulo 11: Controle de Acesso Baseado em Papéis (RBAC, @PreAuthorize)
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Controle de Acesso Baseado em Papéis (RBAC, @PreAuthorize)"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Controle de Acesso Baseado em Papéis (RBAC, @PreAuthorize)
Autenticação vs. autorização. Os capítulos anteriores resolveram “quem é você” (autenticação via JWT). Este capítulo resolve “o que você pode fazer” — autorização. RBAC (Role-Based Access Control) modela isso associando usuários a papéis (roles), e papéis a permissões. Em vez de checar “o usuário X pode deletar pedidos?” caso a caso, checa-se “o usuário X tem o papel ADMIN?” e o papel ADMIN concentra o conjunto de permissões.
Role vs. Authority no Spring Security. O framework não distingue tecnicamente os dois — ambos são representados pela interface GrantedAuthority, uma simples string. A convenção é que uma role seja prefixada com ROLE_ (ex.: ROLE_ADMIN), enquanto uma authority granular representa uma permissão direta sem esse prefixo (ex.: PEDIDO_CANCELAR). Métodos como hasRole("ADMIN") adicionam o prefixo ROLE_ automaticamente por baixo dos panos — por isso hasRole("ADMIN") e hasAuthority("ROLE_ADMIN") são equivalentes, mas hasRole("ROLE_ADMIN") (prefixo duplicado) é um erro comum que sempre falha.
Autorização em nível de URL vs. em nível de método. A SecurityFilterChain do Capítulo 09 já demonstrou autorização por padrão de URL (authorizeHttpRequests(auth -> auth.requestMatchers("/api/v1/admin/**").hasRole("ADMIN"))) — simples e centralizada, mas grosseira: não distingue regras dentro do mesmo endpoint nem protege chamadas internas entre services. A anotação @EnableMethodSecurity (substituta do antigo @EnableGlobalMethodSecurity desde o Security 6) habilita um modelo mais fino baseado em proxies AOP: @PreAuthorize, @PostAuthorize, @Secured e @RolesAllowed passam a funcionar em qualquer bean gerenciado pelo Spring, controller ou service.
@PreAuthorize e SpEL. A expressão dentro de @PreAuthorize é avaliada em Spring Expression Language (SpEL) antes do método executar, com acesso a authentication, aos parâmetros do método (#id) e a beans customizados pelo nome (@meuBean.metodo(...)). Se a expressão retornar false, o método sequer roda e o Spring lança AccessDeniedException, capturada pelo AccessDeniedHandler (403).
@PostAuthorize e segurança em nível de linha. Diferente do @PreAuthorize, essa anotação avalia a expressão depois que o método já executou, tendo acesso ao valor retornado via returnObject. É o mecanismo natural para row-level security: buscar o registro primeiro e só então confirmar que o solicitante é o dono (@PostAuthorize("returnObject.dono == authentication.name")). Tem um custo: a consulta ao banco já ocorreu mesmo que o acesso seja negado — em volumes grandes, prefira filtrar por dono já na query.
RoleHierarchy. Sem hierarquia, um usuário com ROLE_ADMIN não herda automaticamente as permissões de ROLE_USER — cada hasRole(...) checa literalmente aquele papel. O bean RoleHierarchy (RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_MANAGER \n ROLE_MANAGER > ROLE_USER")) resolve isso expandindo as autoridades efetivas do usuário em tempo de avaliação, evitando duplicar hasAnyRole(...) em cada endpoint.
Expressões customizadas. Regras de negócio que não se reduzem a papel/authority (ex.: “só o dono do tenant pode editar”) podem ser encapsuladas em um @Component nomeado e chamado diretamente da SpEL — @PreAuthorize("@tenantGuard.isTenantOwner(#tenantId)") — mantendo a lógica de autorização testável e fora dos controllers.
💻 2. Código de Demonstração Corporativo
package com.empresa.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity // habilita @PreAuthorize, @PostAuthorize e @Secured nos beans do contexto
public class MethodSecurityConfig {
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy("""
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
""");
}
}
package com.empresa.api.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/pedidos")
public class PedidoController {
// Basta ROLE_MANAGER: quem é ADMIN também passa, por herança da RoleHierarchy.
@PreAuthorize("hasRole('MANAGER')")
@GetMapping("/relatorio")
public String relatorioGerencial() {
return "Relatório consolidado de pedidos";
}
@PreAuthorize("hasAuthority('PEDIDO_CANCELAR')")
@PostMapping("/{id}/cancelar")
public String cancelar(@PathVariable Long id) {
return "Pedido " + id + " cancelado";
}
}
package com.empresa.api.service;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
@Service
public class DocumentoService {
record Documento(Long id, String dono, String conteudo) {}
// Consulta primeiro, autoriza depois: só libera o retorno se o dono bater com o usuário logado.
@PostAuthorize("returnObject.dono == authentication.name")
public Documento buscarPorId(Long id) {
return new Documento(id, "carlos.silva", "Conteúdo confidencial do contrato");
}
}