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.