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");
    }
}