Method Security¶
Method Security permite declarar reglas de autorización directamente en los métodos Java con anotaciones, en lugar de solo en las rutas HTTP. Es especialmente útil para lógica de negocio que varía según el rol, el propietario del recurso o el contexto.
Habilitar Method Security en WebFlux¶
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
public class SecurityConfig {
// ... configuración del SecurityWebFilterChain
}
useAuthorizationManager = true es obligatorio en Spring Security 7
La API legacy basada en GlobalMethodSecurityConfiguration está eliminada en Spring Security 7. Usar siempre @EnableReactiveMethodSecurity(useAuthorizationManager = true).
Anotaciones disponibles¶
@PreAuthorize¶
Se evalúa antes de ejecutar el método. Si la expresión devuelve false, lanza AccessDeniedException con HTTP 403.
@RestController
@RequestMapping("/api/v1/usuarios")
public class UsuarioController {
// Solo ADMIN puede ver todos los usuarios
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public Flux<UsuarioDto> listarTodos() {
return usuarioService.findAll();
}
// ADMIN o el propio usuario pueden ver su perfil
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.name")
public Mono<UsuarioDto> obtener(@PathVariable String id) {
return usuarioService.findById(id);
}
// Solo con scope específico del token
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_usuarios:write') and hasRole('ADMIN')")
public Mono<UsuarioDto> crear(@RequestBody @Valid UsuarioRequest request) {
return usuarioService.create(request);
}
// Verificar claims personalizados del JWT
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') and @securityService.puedeEliminar(authentication, #id)")
public Mono<Void> eliminar(@PathVariable String id) {
return usuarioService.delete(id);
}
}
@PostAuthorize¶
Se evalúa después de ejecutar el método. Útil para filtrar el resultado según el usuario.
// Solo puede ver el recurso si es el propietario o ADMIN
@GetMapping("/recursos/{id}")
@PostAuthorize("returnObject.block().propietarioId == authentication.name or hasRole('ADMIN')")
public Mono<RecursoDto> obtenerRecurso(@PathVariable String id) {
return recursoService.findById(id);
}
@PreFilter y @PostFilter¶
Filtran colecciones antes o después de ejecutar el método.
// Solo retorna los recursos del usuario actual si no es ADMIN
@GetMapping("/recursos")
@PostFilter("hasRole('ADMIN') or filterObject.propietarioId == authentication.name")
public Flux<RecursoDto> listarRecursos() {
return recursoService.findAll();
}
Expresiones SpEL disponibles¶
| Expresión | Descripción |
|---|---|
hasRole('ADMIN') |
Tiene el rol (prefijo ROLE_ automático) |
hasAnyRole('ADMIN', 'MANAGER') |
Tiene cualquiera de los roles |
hasAuthority('SCOPE_read') |
Tiene la authority exacta |
hasAnyAuthority('SCOPE_read', 'SCOPE_write') |
Tiene cualquiera de las authorities |
isAuthenticated() |
Usuario autenticado (no anónimo) |
isAnonymous() |
Usuario anónimo |
authentication.name |
Principal name (username o sub del JWT) |
#paramName |
Valor de un parámetro del método |
returnObject |
El objeto retornado (solo @PostAuthorize) |
filterObject |
El objeto actual en la colección (solo @PostFilter) |
Bean de seguridad personalizado¶
Para lógica de autorización compleja, define un bean y refiérelo con @:
@Component("securityService")
public class SecurityService {
public boolean puedeEliminar(Authentication auth, String recursoId) {
if (auth == null) return false;
// Extraer claims del JWT
if (auth instanceof JwtAuthenticationToken jwtAuth) {
Jwt jwt = jwtAuth.getToken();
String realm = jwt.getClaimAsString("iss");
// Solo realm-internal puede eliminar
return realm.contains("realm-internal")
&& jwtAuth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
return false;
}
public boolean esPropioPerfil(Authentication auth, String userId) {
return auth != null && userId.equals(auth.getName());
}
}
Pruebas con @WithMockUser¶
@WebFluxTest(UsuarioController.class)
@Import(SecurityConfig.class)
class UsuarioControllerTest {
@Autowired
private WebTestClient webClient;
@Test
@WithMockUser(roles = "ADMIN")
void listarTodos_comoAdmin_devuelve200() {
webClient.get().uri("/api/v1/usuarios")
.exchange()
.expectStatus().isOk();
}
@Test
@WithMockUser(roles = "USER")
void listarTodos_comoUser_devuelve403() {
webClient.get().uri("/api/v1/usuarios")
.exchange()
.expectStatus().isForbidden();
}
@Test
void listarTodos_sinAutenticacion_devuelve401() {
webClient.get().uri("/api/v1/usuarios")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
void listarTodos_conJwtValido_devuelve200() {
webClient.mutateWith(mockJwt()
.authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))
.jwt(jwt -> jwt.subject("user-123")))
.get().uri("/api/v1/usuarios")
.exchange()
.expectStatus().isOk();
}
}