CodeGym /Cursos /Spring Security /Reglas de acceso a nivel de solicitud de API

Reglas de acceso a nivel de solicitud de API

Spring Security
Nivel 18, Lección 0
Disponible

1. Autorización a nivel de solicitud y mapa de acceso

Si miramos la seguridad con ojos de principiante, es muy fácil empezar «desde abajo»: abrir la documentación, encontrar un bonito ejemplo e insertar en la configuración algo como "/api/**".authenticated(). Esto funciona... hasta la primera pregunta «¿por qué de repente se cerraron los artículos públicos?» o «¿por qué un editor puede entrar al panel de administración?». La autorización a nivel de solicitud (request-level) es como la seguridad en la entrada: debe ser estricta, pero aún más importante: predecible y legible.

Los códigos REST-friendly 401 y 403 ya los tenemos convertidos a un formato JSON comprensible. Ahora necesitamos una capa que resuelva una pregunta anterior: qué solicitudes deben llegar a esta bifurcación y cuáles deben cortarse ya en la entrada.

Autorización a nivel de solicitud (request-level) es una verificación de acceso que se realiza según la forma de la solicitud HTTP entrante: ruta (/api/...), método (GET/POST/...) y el nivel de acceso mínimo necesario. Idea importante: en este nivel todavía no nos interesa «qué hay dentro del controlador» ni cómo está organizado. Nos interesa otra cosa: si esta solicitud puede siquiera pasar a la aplicación más allá de los filtros.

Una analogía útil (no abuso, lo prometo): imaginen un concierto. Las reglas request-level son los torniquetes y los guardias en la entrada, que verifican el boleto y la pulsera. Pero no verifican que hayas ocupado exactamente tu asiento en la fila 14. «Tu propio asiento» es una verificación posterior: las reglas a nivel de servicio y ownership. Hoy ponemos orden en los torniquetes.

Para no verlo de forma abstracta, tengan en mente este breve esquema:

flowchart LR
  A["Petición HTTP"] --> B["SecurityFilterChain"]
  B -->|reglas request-level| C{"¿Entrada permitida?"}
  C -->|sí| D["Controller / Service"]
  C -->|no| E["401 o 403 en JSON"]

A la izquierda — la solicitud, a la derecha — su lógica de negocio. Entre ellos — Spring Security, que debe tomar la primera decisión: «dejamos pasar» o «no». Y para que esta decisión sea significativa, necesitamos no una dispersión de URLs aleatorios, sino un mapa de zonas de acceso.

2. Matriz de acceso: traducimos el negocio a HTTP

Hay la tentación de pensar que la matriz de acceso (access matrix) es un «papel del primer día del curso» que hicimos para marcar la casilla, y ahora vivimos con DSL y anotaciones. En realidad, es todo lo contrario: cuanto más grande es el proyecto, más agradecidos estarán por haber distribuido honestamente el acceso por zonas. De lo contrario, la configuración se convierte en «arqueología»: la regla existe, pero no se entiende por qué es así, y da miedo tocarla.

Para Secure Content Platform API tenemos zonas de negocio bastante naturales, y es una gran suerte. La aplicación misma sugiere la estructura de las reglas request-level:

  • lectura pública del contenido publicado (usuario anónimo lee artículos);
  • zona personal del usuario (perfil, «quién soy», avatar);
  • zona de borradores (generalmente privada, requiere inicio de sesión en la entrada);
  • zona de moderación (editor/admin);
  • zona de administración de usuarios (admin).

A nivel request-level casi siempre empezamos con la pregunta: ¿qué nivel mínimo de acceso se necesita para que la solicitud “tenga sentido”? Por ejemplo, GET /api/public/articles tiene sentido incluso sin inicio de sesión. En cambio, PATCH /api/me/profile sin inicio de sesión es un sinsentido: «modificar el perfil de un usuario inexistente».

Es conveniente verlo una vez en una tabla. Importante: esto no es «autorización final para todos los casos», sino el esqueleto de las reglas de entrada.

Zona API Ejemplos de endpoints Regla mínima de entrada
Public read GET /api/public/articles,
GET /api/public/articles/{slug}
permitAll() (normalmente solo para GET)
“Yo” / Profile /api/me/** authenticated()
Drafts / zona de trabajo /api/drafts/** authenticated()
Moderación /api/editor/** regla privilegiada separada (hasRole("EDITOR") o hasAuthority(...))
Admin /api/admin/** regla privilegiada separada (hasRole("ADMIN") o hasAuthority(...))

Observen un detalle sutil: en la entrada de /api/drafts/** a menudo ponemos authenticated(). No es porque cualquier usuario conectado pueda realizar cualquier operación con cualquier borrador. Es porque sin inicio de sesión la solicitud no debería llegar allí en absoluto. «¿Se puede leer/modificar un borrador específico?» es el siguiente nivel de protección, y hoy deliberadamente no lo resolvemos.

Así obtenemos un modelo mental de trabajo: las reglas request-level responden a la pregunta «¿en qué puerta intentas entrar?», y no «¿qué exactamente quieres hacer con un objeto específico en la base de datos?».

3. De las zonas a requestMatchers

Cuando escriben por primera vez authorizeHttpRequests(...), existe el riesgo de hacer una configuración técnicamente correcta pero humanamente ilegible. Se verá como una «lista de excepciones» sin estructura. El problema no es que Spring Security sea malo. El problema es que la seguridad es un área donde, al cabo de un mes, no te fías ni de ti mismo: «¿esta regla la quería exactamente así? ¿o solo intentaba “arreglar el 403”?».

El estilo correcto para una configuración request-level a nuestro nivel es agrupar las reglas por zonas, y colocarlas de arriba a abajo como se lee su contrato API. Normalmente el orden es: primero las excepciones públicas (lo que se puede sin inicio de sesión), luego las zonas personales, luego las zonas privilegiadas, y al final — la regla de cierre (anyRequest().denyAll()), para no dejar accidentalmente un «agujero» para nuevos endpoints.

Empecemos con el bloque mínimo más importante: public read. Incluso si luego complican el proyecto, la zona pública casi siempre se destaca por separado.

import org.springframework.http.HttpMethod;

// dentro de authorizeHttpRequests(auth -> auth ...)

// Zona pública: permitimos solo lectura de artículos publicados
auth.requestMatchers(
        HttpMethod.GET, // importante: abrimos solo GET, no cualquier método
        "/api/public/articles",
        "/api/public/articles/**"
    )
    .permitAll(); // anónimos pueden

Aquí se ve claro el sentido: los artículos publicados son públicos, pero solo lectura (GET). No abrimos «todo /api/public/** para todo», sino que hacemos una regla específica.

Luego aparece la parte mínimamente privada: la zona personal y los borradores. A nivel request-level por ahora solo decimos: «sin inicio de sesión — no se permite».

// dentro de authorizeHttpRequests(auth -> auth ...)

// Zona personal: sin autenticación las solicitudes aquí no tienen sentido
auth.requestMatchers("/api/me", "/api/me/**").authenticated();

// Borradores: en la entrada requieren inicio de sesión, y la pertenencia (ownership) se resuelve después (a nivel de servicio)
auth.requestMatchers("/api/drafts", "/api/drafts/**").authenticated();

Y solo después de esto separamos las zonas privilegiadas del «usuario normalmente conectado». Esto es fundamental: editor y admin no son «simplemente authenticated más un poco de magia».

// dentro de authorizeHttpRequests(auth -> auth ...)

// Zonas privilegiadas — reglas separadas para no mezclarlas con 'simplemente conectado'
auth.requestMatchers("/api/editor/**").hasRole("EDITOR");
auth.requestMatchers("/api/admin/**").hasRole("ADMIN");

Sí, usamos roles. Y está bien como fijación inicial de la zona. En nuestro proyecto, más adelante aparecerán authorities y reglas más precisas (por ejemplo, en algún lugar habrá draft:review, en otro draft:publish). Pero incluso si luego ajustan los permisos, el límite mismo de la zona debe ser visible ya ahora: /api/editor/** y /api/admin/** no son parte de la «zona authenticated normal».

Y por último — el límite importante del tema de hoy. Incluso si /api/drafts/** requiere authenticated(), esto no significa que el usuario pueda leer borradores ajenos. La regla request-level solo dice: «se puede pasar al pasillo solo con pase». Pero en qué oficina se puede entrar se decide después. Y tan pronto como dentro de una zona aparecen diferentes operaciones — lectura, modificación, publicación, — una simple división por prefijos de ruta ya no es suficiente. Entonces habrá que afinar las reglas mediante la combinación de método HTTP y patrón de ruta.

4. Esqueleto del mapa de Secure Content Platform API

Ahora daremos un paso importante: reuniremos el mapa de acceso request-level mínimamente viable en un solo lugar. Digo «mínimamente viable» a propósito, porque hoy estamos construyendo el esqueleto. No hacemos «todo perfecto y para siempre», hacemos «claro, estructurado, sin agujeros lógicos». Todavía no es la configuración completa de la aplicación: aquí nos importa fijar las zonas básicas y el orden general de las reglas.

Imaginemos que los manejadores de errores REST-friendly ya están en su lugar: AuthenticationEntryPoint para 401 y AccessDeniedHandler para 403. En esta lección no los reescribimos, solo nos aseguramos de que las nuevas reglas request-level sigan dando las mismas respuestas predecibles.

Un mini-esqueleto de configuración podría verse así (solo se muestran las líneas clave, sin ruido innecesario):

import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        // Public read: abrimos solo GET en artículos públicos
        .requestMatchers(HttpMethod.GET, "/api/public/articles/**").permitAll()

        // Zona de usuario y borradores: en la entrada se necesita inicio de sesión
        // (y los derechos específicos sobre un recurso específico son el siguiente nivel)
        .requestMatchers("/api/me/**", "/api/drafts/**").authenticated()

        // Zonas privilegiadas: por separado para no confundirlas con 'simplemente authenticated'
        .requestMatchers("/api/editor/**").hasRole("EDITOR")
        .requestMatchers("/api/admin/**").hasRole("ADMIN")

        // Default deny: si la ruta no está descrita explícitamente, está cerrada
        .anyRequest().denyAll()
    );

    // Construimos SecurityFilterChain con las reglas dadas
    return http.build();
}

Aquí hay varias propiedades importantes que hacen que la configuración sea «humana»:

En primer lugar, se lee de arriba a abajo como una mini-documentación. Si soy un desarrollador nuevo en el equipo y abro este archivo, entiendo de inmediato: «bien, los artículos son públicos, la zona personal y los borradores requieren inicio de sesión, luego hay zonas separadas editor y admin, todo lo demás está cerrado».

En segundo lugar, aquí se ve que no se puede acceder a las zonas privilegiadas solo porque “estoy conectado”. Esto no resuelve todos los problemas de seguridad, pero elimina una gran clase de errores cuando alguien abre accidentalmente la moderación a usuarios normales.

En tercer lugar, esto prepara el terreno para el siguiente paso: afinar las reglas por método HTTP y patrones de ruta más específicos. Pero no estamos obligados a comenzar con eso. Primero — zonas y esqueleto, luego — detalle.

Y por último — el límite importante del tema de hoy. Incluso si /api/drafts/** requiere authenticated(), esto no significa que el usuario pueda leer borradores ajenos. La regla request-level solo dice: «se puede pasar al pasillo solo con pase». Pero en qué oficina se puede entrar se decide después. Y tan pronto como dentro de una zona aparecen diferentes operaciones — lectura, modificación, publicación, — una simple división por prefijos de ruta ya no es suficiente. Entonces habrá que afinar las reglas mediante la combinación de método HTTP y patrón de ruta.

5. Errores típicos en las reglas request-level

Casi todos los problemas en la configuración request-level ocurren no porque la persona «no sepa Spring Security», sino porque piensa no en zonas, sino en líneas sueltas. Como resultado, la configuración parece un «montón de parches» en diferentes lugares: hoy pegamos uno, mañana otro, y el mapa de acceso general nunca aparece.

Error nº1: empezar con "/api/**".authenticated() y luego intentar «recortar» lo público mediante excepciones.
Esa regla es ciertamente cómoda de escribir primero, pero forma un mal hábito: la configuración se convierte en una lista de excusas, no en un mapa. Es mucho mejor comenzar con la zona public read y solo luego cubrir el resto con una regla más general. Este es un pequeño paso psicológico, pero enorme en legibilidad.

Error nº2: mezclar la zona authenticated y la zona privileged en un solo montón.
Cuando /api/editor/** y /api/admin/** están «simplemente por ahí» dentro de un authenticated() general, es muy fácil abrir accidentalmente la moderación a usuarios normales, especialmente si el orden de los matchers se desplaza. Separen las zonas privileged en bloques separados y con reglas separadas, incluso si por ahora es un burdo hasRole("EDITOR").

Error nº3: olvidar que request-level es solo una verificación de entrada, e intentar resolver la ownership a través de la URL.
Un principiante a menudo quiere «vencer la seguridad con una línea», y empieza a inventar patrones de ruta complejos para «entender al propietario por la URL». En un proyecto real esto es casi siempre frágil. A nivel request-level es suficiente cerrar la entrada a la zona (/api/drafts/** solo para conectados), y resolver «propio/ajeno» donde se tenga acceso a los datos del objeto.

Error nº4: no poner la regla de cierre y dejar accidentalmente rutas impredecibles.
Si al final no hay anyRequest().denyAll(), ustedes mismos pueden no notar cómo un nuevo endpoint se vuelve accesible de una manera inesperada. La regla de cierre no es «maldad por maldad», sino una forma de documentar la intención: «todo lo que no está descrito está cerrado».

Error nº5: hacer una configuración técnicamente correcta pero ilegible.
Se pueden escribir 30 requestMatchers en orden aleatorio, e incluso funcionará. Pero al mes empezarán a temer modificar ese archivo, porque cualquier cambio podría «romper la magia». Procuren que la configuración se lea como una mini-especificación: public, user-zone, privileged-zone, luego deny-by-default. Esto ahorra tiempo y nervios más que cualquier tutorial «perfecto».

1
Tarea
Spring Security, nivel 18, lección 0
Bloqueada
Mapa de acceso para la API de noticias
Mapa de acceso para la API de noticias
1
Tarea
Spring Security, nivel 18, lección 0
Bloqueada
Request-level mapa para el servicio de solicitudes de soporte
Request-level mapa para el servicio de solicitudes de soporte
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION