La autorización a nivel de URL con authorizeHttpRequests cubre la mayoría de casos, pero deja huecos cuando la decisión depende del dominio: el dueño del recurso, el estado de un pedido o la pertenencia a un equipo. La method security de Spring Security 7.x resuelve ese hueco con cuatro anotaciones que evalúan expresiones SpEL justo antes o después de invocar un método de servicio.
Activar method security
A diferencia de Spring Security 5.x, donde había que extender GlobalMethodSecurityConfiguration, ahora basta con una anotación sobre cualquier @Configuration.
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig { }
Por defecto se habilitan @PreAuthorize y @PostAuthorize. Si quieres @Secured (estilo legacy) o JSR-250 (@RolesAllowed), actívalos de forma explícita.
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
No actives
prePostEnabled = false. Las anotaciones modernas con SpEL son la opción recomendada y la única que ofrece toda la expresividad.
PreAuthorize y PostAuthorize
@PreAuthorize evalúa la expresión antes de invocar el método. Si devuelve false, lanza AccessDeniedException y el método nunca se ejecuta.
@Service
public class TareaService {
@PreAuthorize("hasRole('ADMIN')")
public void borrarTodo() { ... }
@PreAuthorize("hasAuthority('SCOPE_tareas:write')")
public Tarea crear(NuevaTareaCommand cmd) { ... }
@PreAuthorize("#user.id == authentication.principal.id")
public void actualizarPerfil(Usuario user) { ... }
}
Las dos primeras son básicas: comprueban roles o autoridades. La tercera ya es interesante porque accede al parámetro del método (#user) y al objeto de autenticación (authentication).
@PostAuthorize evalúa la expresión después del método y puede inspeccionar el valor de retorno con la variable returnObject. Es útil cuando la decisión depende de los datos cargados.
@PostAuthorize("returnObject.propietario == authentication.name")
public Documento obtener(Long id) {
return repo.findById(id).orElseThrow();
}
@PostAuthorizecarga el objeto de la base de datos antes de decidir. Si el atacante puede invocar el método mil veces para enumerar IDs ajenos, prefiere combinar con@PreAuthorizepara filtrar por permisos antes de tocar la BD.
PreFilter y PostFilter sobre colecciones
@PreFilter y @PostFilter operan sobre colecciones y eliminan los elementos que no cumplen la expresión. La variable filterObject representa cada elemento.
@PreFilter("filterObject.propietario == authentication.name")
public void importar(List<Tarea> tareas) {
tareas.forEach(repo::save);
}
@PostFilter("filterObject.visibilidad == 'PUBLICA' or filterObject.propietario == authentication.name")
public List<Tarea> listar() {
return repo.findAll();
}
@PreFilter modifica la lista de entrada y el método solo procesa los elementos válidos. @PostFilter aplica la criba sobre el resultado y devuelve solo lo que el usuario puede ver.
@PostFiltercarga toda la colección antes de filtrar. En catálogos grandes prefiere mover la condición alWHEREde la consulta JPA mediante unSpecification. El filtro a posteriori es cómodo, pero ineficiente.
Variables y funciones de la expresión SpEL
Spring Security expone un conjunto de variables y funciones integradas que cubren los casos comunes.
authentication: el objetoAuthenticationactivo.principal: el principal de la autenticación, normalmenteUserDetailso unOidcUser.hasRole('NOMBRE'): cumple si el usuario tieneROLE_NOMBRE.hasAuthority('SCOPE_tareas:read'): cumple si el usuario tiene esa autoridad exacta.hasAnyRole('A', 'B')yhasAnyAuthority('SCOPE_a', 'SCOPE_b'): variantes con OR.isAuthenticated(),isFullyAuthenticated(),isAnonymous().permitAll(),denyAll().
Para pasar parámetros del método se usa la sintaxis #nombre, que coincide con el nombre del parámetro siempre que el código se compile con -parameters (Maven y Gradle lo activan por defecto en Spring Boot moderno).
@PreAuthorize("#equipoId == authentication.principal.equipoId")
public List<Proyecto> proyectosDelEquipo(Long equipoId) { ... }
Custom AuthorizationManager con beans propios
Cuando la lógica supera lo que cabe en una expresión, conviene desplazarla a un bean de servicio y referenciarlo desde la expresión con la sintaxis @nombreBean.metodo(args).
@Component("permisos")
public class PermisosService {
private final ProyectoRepository repo;
public PermisosService(ProyectoRepository repo) { this.repo = repo; }
public boolean puedeEditarProyecto(Authentication auth, Long proyectoId) {
if (auth == null) return false;
return repo.findById(proyectoId)
.map(p -> p.getResponsable().equals(auth.getName())
|| p.getColaboradores().contains(auth.getName()))
.orElse(false);
}
}
Y la anotación delega en el bean.
@PreAuthorize("@permisos.puedeEditarProyecto(authentication, #proyectoId)")
public Proyecto actualizar(Long proyectoId, ActualizarProyectoCmd cmd) { ... }
Esta es la forma idiomática de implementar autorización basada en dominio en Spring Security moderno. Mantiene la anotación legible y desacopla la lógica de negocio de la capa de seguridad.
AuthorizationManager: el reemplazo programático
A partir de Spring Security 6, las anotaciones se pueden sustituir por un AuthorizationManager programático cuando se prefiere registrar todas las reglas en un único punto.
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
@Bean
Advisor proyectosAuthorization(ProyectosAuthorizationManager manager) {
var pointcut = new AnnotationMatchingPointcut(null, RequiereAccesoProyecto.class);
return new AuthorizationManagerBeforeMethodInterceptor(pointcut, manager);
}
El AuthorizationManager recibe el contexto de invocación, lo decide en código Java puro y devuelve un AuthorizationDecision. Es el patrón preferido cuando hay decenas de reglas dinámicas que no encajan bien en SpEL.
Combinaciones avanzadas con SpEL
SpEL admite operadores lógicos (and, or, not), aritméticos y de colección. Algunos patrones recurrentes.
- OR de roles con condición de dominio:
@PreAuthorize("hasRole('ADMIN') or @permisos.esResponsable(authentication, #id)")
public Tarea editar(Long id) { ... }
- Filtro por jerarquía de organización:
@PreAuthorize("#req.empresa == authentication.principal.empresa or hasRole('SUPERADMIN')")
public Informe generar(InformeRequest req) { ... }
- Acceso a propiedades anidadas:
@PostAuthorize("returnObject.proyecto.responsable == authentication.name")
public Tarea obtener(Long id) { ... }
Cuando la expresión empieza a costar leerla, muévela a un bean. La regla mental es: si no cabe cómodamente en una línea, ya es demasiado larga para SpEL.
Excepciones y respuesta HTTP
Cuando una expresión falla, Spring Security lanza AccessDeniedException. Por defecto se traduce a HTTP 403 Forbidden, pero conviene personalizar la respuesta para que el cliente reciba una estructura coherente con el resto de la API.
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Map<String, Object>> denied(AccessDeniedException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of(
"error", "forbidden",
"message", "No tienes permiso para esta operación"
));
}
}
No reveles el motivo exacto del rechazo. Mensajes como "necesita rol ADMIN" facilitan la enumeración de privilegios.
Testing con @WithMockUser y MockMvc
Las pruebas unitarias de los métodos protegidos se escriben con @WithMockUser para simular un usuario en el SecurityContext.
@SpringBootTest
class TareaServiceTest {
@Autowired TareaService service;
@Test
@WithMockUser(roles = "ADMIN")
void adminPuedeBorrar() {
assertThatNoException().isThrownBy(service::borrarTodo);
}
@Test
@WithMockUser(authorities = "SCOPE_tareas:write")
void usuarioConScopePuedeCrear() {
assertThat(service.crear(new NuevaTareaCommand("Test"))).isNotNull();
}
@Test
@WithMockUser(roles = "USER")
void userNoPuedeBorrar() {
assertThatThrownBy(service::borrarTodo)
.isInstanceOf(AccessDeniedException.class);
}
}
Para tests de controlador con expresiones que usan #parametro, recuerda compilar con -parameters (Spring Boot lo configura por defecto).
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Security
Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Aplicar method security con PreAuthorize, PostAuthorize, PreFilter y PostFilter, escribir expresiones SpEL avanzadas con beans personalizados y combinarlas con AuthorizationManager para reglas de dominio.