Controle de Acesso Customizado utilizando Spring AOP e Anotações

A Porgramação Orientada a Aspectos (AOP) é um paradigma que complementa a Programação Orientada a Objetos (OOP), permitindo a separação de preocupações transversais (cross-cutting concerns). Em sistemas corporativos, funcionalidades como auditoria, tratamento de exceções e, principalmente, segurança (autenticação e autorização) são exemplos clássicos de lógica que costuma se repetir em diversos métodos.

Em vez de replicar blocos de validação de permissões em cada método do Controller, utilizamos o Spring AOP para "interceptar" a execução. Através de anotações personalizadas, ganhamos a flexibilidade de definir diferentes níveis de acesso para cada endpoint de forma declarativa e limpa.

Estrutura da Solução

A implementação baseia-se em três pilares: a definição de uma anotação que servirá como marcador (Pointcut), a lógica de interceptação (Aspect) e a aplicação prática nos métodos de negócio.

1. Definição da Anotação de Permissão

Primeiro, criamos a interface que será utilizada para marcar os métodos que exigem verificação. Ela permite parametrizar quais módulos ou funcionalidades o usuário deve possuir.


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessControl {
   // Define o nível básico de validação
   SecurityLevel level() default SecurityLevel.STRICT;

   // Identificadores de funcionalidades específicas
   String[] permissions() default {};

   // Identificadores de módulos do sistema
   String[] modules() default {};
}
   

2. Implementação do Aspecto de Segurança

O Aspecto é onde a mágica acontece. Ele captura a execução dos métodos anotados com @AccessControl, extrai os metadados da anotação e valida se o usuário atual possui as credenciias necessárias.


@Aspect
@Component
public class SecurityInterceptorAspect {

   private static final Logger log = LoggerFactory.getLogger(SecurityInterceptorAspect.class);

   @Pointcut("@annotation(com.exemplo.projeto.annotation.AccessControl)")
   public void securityPointcut() {
   }

   @Around("securityPointcut()")
   public Object validateAccess(ProceedingJoinPoint joinPoint) throws Throwable {
       MethodSignature signature = (MethodSignature) joinPoint.getSignature();
       Method method = signature.getMethod();
       AccessControl config = method.getAnnotation(AccessControl.class);

       log.debug("Iniciando verificação de segurança para: {}", method.getName());

       // Se configurado para ignorar validação, prossegue imediatamente
       if (config.level() == SecurityLevel.PUBLIC) {
           return joinPoint.proceed();
       }

       UserSession currentUser = SessionManager.getCurrentUser();
       if (currentUser == null) {
           return redirectToLogin();
       }

       // Validação por Nível de Acesso ou Permissões Específicas
       if (config.level() == SecurityLevel.STRICT) {
           boolean hasPermission = checkPermissions(currentUser, config.permissions());
           boolean hasModuleAccess = checkModules(currentUser, config.modules());

           if (!hasPermission && !hasModuleAccess) {
               return respondWithForbidden(method);
           }
       }

       return joinPoint.proceed();
   }

   private boolean checkPermissions(UserSession user, String[] required) {
       if (required.length == 0) return true;
       return Arrays.stream(required).anyMatch(user::hasRole);
   }

   private boolean checkModules(UserSession user, String[] modules) {
       if (modules.length == 0) return true;
       return Arrays.stream(modules).anyMatch(user::hasModuleAccess);
   }

   private Object respondWithForbidden(Method method) {
       boolean isRest = method.isAnnotationPresent(ResponseBody.class);
       if (isRest) {
           Map<String, Object> error = new HashMap<>();
           error.put("status", "error");
           error.put("message", "Acesso negado: privilégios insuficientes.");
           return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
       }
       return "redirect:/errors/403";
   }

   private Object redirectToLogin() {
       // Lógica para injetar script de redirecionamento ou retornar status 401
       HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
       response.setContentType("text/html;charset=UTF-8");
       try {
           response.getWriter().write("<script>top.location.href='/login';</script>");
       } catch (IOException e) {
           log.error("Erro ao redirecionar", e);
       }
       return null;
   }
}
   

3. Aplicação no Controller

Com o aspecto configurado, basta adicionar a anotação aos métodos desejados. O código de negócio fica focado apenas na sua responsabilidade principal, enquanto a segurança é tratada de forma transparente pela camada de AOP.


@RestController
@RequestMapping("/api/usuarios")
public class UserManagementController {

   @PostMapping("/alterar-senha")
   @AccessControl(level = SecurityLevel.LOGGED_ONLY)
   public ResponseEntity<?> changePassword(@RequestBody PasswordUpdateDTO dto) {
       // A segurança já foi validada pelo Aspecto antes de chegar aqui
       userService.updatePassword(dto);
       return ResponseEntity.ok("Senha atualizada com sucesso");
   }

   @DeleteMapping("/{id}")
   @AccessControl(permissions = {"ADMIN_DELETE", "SUPERUSER"})
   public ResponseEntity<?> removeUser(@PathVariable Long id) {
       userService.deleteById(id);
       return ResponseEntity.noContent().build();
   }
}
   

Conclusão

Esta abordagem centraliza a lógica de autorização, facilitando a manutenção e garantindo que novas rotas possam ser protegidas com esforço mínimo. Ao utilizar anotações customizadas, o código torna-se mais legível e menos propenso a falhas de segurança causadas por esquecimento de verificações manuais.

Tags: Spring Boot Spring AOP java Security Custom Annotations

Publicado em 7-28 14:43