El control de acceso basado en anotaciones personalizadas y Programación Orientada a Aspectos (AOP) es una técnica estándar en el ecosistema Spring Boot. Sin embargo, cuando los requisitos de seguridad se vuelven complejos —como verificar múltiples roles, rangos horarios o permisos dinámicos—, las implementaciones estáticas suelen volverse rígidas y difíciles de mantener. El uso de Spring Expression Language (SpEL) ofrece una solución elegente para dotar de dinamismo y flexibilidad a estas validaciones.
Limitaciones del enfoque tradicional
Normalmente, un desarrollador crea una anotación simple que recibe un identificador de permiso. No obstante, en escenarios reales, las necesidades suelen ser más variadas:
- Permitir el acceso solo si el usuario tiene una combinación específica de roles.
- Restringir el acceso a ciertos métodos según la hora del sistema.
- Validar si el usuario es el propietario del recurso basándose en los parámetros del método.
- Implementar una política de "denegar todo" excepto para el administrador.
Intentar cubrir estos casos con múltiples anotaciones o lógica if-else dentro del Aspecto genera un código desordenado. Aquí es donde SpEL permite tratar la lógica de seguridad como una expresión evaluable en tiempo de ejecución.
Definición de la anotación personalizada
En lugar de pasar un simple código de permiso, definiremos una anotación que acepte una expresión SpEL.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
/**
* Expresión SpEL a evaluar.
* Ejemplos:
* - isGuest()
* - hasRole('ADMIN')
* - permitAll()
* - checkTime(9, 18)
*/
String value();
}
El motor de validación: SecurityEvaluator
Para que SpEL reconozca nuestras funciones personalizadas, necesitamos una clase que contenga la lógica de negocio. Cada método público de esta clase podrá ser invocado directamente desde la expresión definida en la anotación.
public class SecurityEvaluator {
public boolean permitAll() {
return true;
}
public boolean isAuthorized() {
// Lógica para verificar si el usuario está autenticado
return SecurityContext.getCurrentUser() != null;
}
public boolean hasRole(String roleName) {
User user = SecurityContext.getCurrentUser();
return user != null && user.getRoles().contains(roleName);
}
public boolean checkTime(int startHour, int endHour) {
int currentHour = LocalDateTime.now().getHour();
return currentHour >= startHour && currentHour <= endHour;
}
public boolean hasAllRoles(String... roles) {
for (String role : roles) {
if (!hasRole(role)) return false;
}
return true;
}
}
Implementación del Aspecto (AOP)
El núcleo de esta solución es el Aspecto que intercepta las peticiones. Su función es extraer la expresión de la anotación, configurar el contexto de SpEL con nuestro SecurityEvaluator y ejecutar la evaluación.
@Aspect
@Component
public class SecurityInterceptor {
private final ExpressionParser parser = new SpelExpressionParser();
private final ApplicationContext applicationContext;
public SecurityInterceptor(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Around("@annotation(accessLimit) || @within(accessLimit)")
public Object authorize(ProceedingJoinPoint joinPoint, AccessLimit accessLimit) throws Throwable {
if (validateExpression(joinPoint, accessLimit.value())) {
return joinPoint.proceed();
}
throw new RuntimeException("Acceso denegado: Requisitos de seguridad no cumplidos");
}
private boolean validateExpression(ProceedingJoinPoint point, String expressionStr) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// Inicializar contexto con nuestro evaluador de seguridad
StandardEvaluationContext context = new StandardEvaluationContext(new SecurityEvaluator());
// Permitir que la expresión acceda a Beans de Spring si fuera necesario
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
// Mapear los argumentos del método interceptado como variables de SpEL
Object[] args = point.getArgs();
String[] paramNames = signature.getParameterNames();
if (paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
Expression expression = parser.parseExpression(expressionStr);
Boolean result = expression.getValue(context, Boolean.class);
return result != null && result;
}
}
Uso práctico en Controladores
Gracias a esta estructura, la definición de permisos en los controladores se vuelve sumamente descriptiva y potente. Podemos referenciar métodos del evaluador e incluso pasar parámetros dinámicos.
@RestController
@RequestMapping("/api/reports")
@AccessLimit("isAuthorized()")
public class ReportController {
@GetMapping("/public")
@AccessLimit("permitAll()")
public ResponseEntity getPublicData() {
return ResponseEntity.ok("Datos públicos");
}
@GetMapping("/admin-only")
@AccessLimit("hasRole('ADMIN')")
public ResponseEntity getAdminData() {
return ResponseEntity.ok("Datos restringidos");
}
@PostMapping("/restricted-schedule")
@AccessLimit("checkTime(8, 20) and hasRole('OPERATOR')")
public ResponseEntity performOperation() {
return ResponseEntity.ok("Operación realizada en horario permitido");
}
}
Conclusión
Integrar SpEL en el flujo de autorización transforma un sistema rígido en uno dinámico. La principal ventaja es la separación de responsabilidades: el Aspecto maneja la infraestructura de interceptación, mientras que la clase SecurityEvaluator centraliza las reglas de negocio. Esto no solo simplifica la lectura del código, sino que facilita la expansión del sistema de pemrisos ante nuevos requerimientos técnicos o de negocio.