Spring SecurityLección 02
Configurar la cadena con el DSL
Toda la configuración moderna es un bean SecurityFilterChain construido con HttpSecurity y lambdas. Cómo leerlo, cómo se evalúan las reglas y cómo tener varias cadenas.
Al terminar sabrás
- Leer y escribir un
SecurityFilterChaincon el DSL de lambdas de Spring Security 7. - Ordenar reglas de autorización sabiendo que gana la primera que coincide.
- Usar los patrones de
PathPatternRequestMatchersin errores de arranque. - Separar API y web con varias cadenas,
securityMatchery@Order.
En Spring Security 7 no hay clase base que extender (WebSecurityConfigurerAdapter desapareció en la 6.0) ni .and() para encadenar (eliminado en la 7.0). Toda la configuración es un @Bean que devuelve un SecurityFilterChain, construido con HttpSecurity y lambdas.
La forma canónica
Sección titulada «La forma canónica»@Configuration@EnableWebSecurityclass SecurityConfig {
@Bean SecurityFilterChain api(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 1. reglas de autorización .requestMatchers(HttpMethod.GET, "/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) // 2. regla por defecto: siempre al final .httpBasic(Customizer.withDefaults()) // 3. un mecanismo de autenticación .formLogin(form -> form.disable()) // 4. apaga lo que no usas .sessionManagement(s -> s .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); return http.build(); // 5. congela la cadena }}Cómo leerlo:
- Cada método de
http(authorizeHttpRequests,httpBasic,csrf,cors,sessionManagement,exceptionHandling,oauth2ResourceServer…) es un configurer: añade, quita o configura uno o más filtros de la cadena. - La lambda recibe el configurer y lo ajusta.
Customizer.withDefaults()significa “actívalo con su configuración por defecto”. x -> x.disable()quita ese filtro. No es lo mismo que no llamarlo: CSRF, cabeceras de seguridad y gestión de sesión están activos por defecto y solo desaparecen si los desactivas.http.build()congela la cadena.
Las reglas se evalúan de arriba abajo
Sección titulada «Las reglas se evalúan de arriba abajo»authorizeHttpRequests funciona como un if / else if: gana la primera regla que coincide. Por eso lo específico va arriba y anyRequest() siempre al final (si lo pones antes de otra regla, la aplicación no arranca).
Evita
.authorizeHttpRequests(auth -> auth .requestMatchers("/api/**").authenticated() // nunca se evalúa: /api/** ya coincidió arriba .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().denyAll())Prefiere
.authorizeHttpRequests(auth -> auth .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll())Matchers en Spring Security 7
Sección titulada «Matchers en Spring Security 7»Los strings de requestMatchers("...") se interpretan con PathPatternRequestMatcher, el mismo motor de patrones que usa Spring MVC para mapear tus controladores. AntPathRequestMatcher y MvcRequestMatcher ya no existen.
| Patrón | Coincide con | Ojo |
|---|---|---|
/api/notes | Solo esa ruta exacta | |
/api/notes/* | /api/notes/5, pero no /api/notes/5/tags | * = un segmento |
/api/notes/** | Todo lo que cuelga de /api/notes (y la propia ruta) | ** solo al final del patrón |
/api/**/admin | — | Error al arrancar: ** en medio ya no se acepta |
/api/notes/{id} | /api/notes/5 | Captura variables, útiles en reglas con access(...) |
Puedes filtrar también por método: requestMatchers(HttpMethod.DELETE, "/api/notes/**").
Las reglas disponibles
Sección titulada «Las reglas disponibles»| Regla | Significa |
|---|---|
permitAll() | Cualquiera, incluido anónimo (los filtros se ejecutan igual) |
denyAll() | Nadie |
authenticated() | Cualquier usuario no anónimo |
hasRole("ADMIN") | Tiene la authority ROLE_ADMIN |
hasAnyRole("ADMIN", "MOD") | Tiene alguna de ellas |
hasAuthority("notes:write") | Tiene esa authority exacta, sin prefijo |
hasAllAuthorities(...) / hasAllRoles(...) | Tiene todas (nuevo en la 7.0) |
access(authorizationManager) | Lógica propia (lo verás en autorización) |
Varias cadenas: securityMatcher y @Order
Sección titulada «Varias cadenas: securityMatcher y @Order»Cuando partes de la aplicación necesitan seguridad distinta (una API stateless con tokens y un panel web con sesión, o el Actuator), declaras varias SecurityFilterChain:
- Petición
GET /api/notes - @Order(1)Cadena API
securityMatcher("/api/**")→ coincidestatelessHTTP Basic / JWTsin CSRF - @Order(2)Cadena websin securityMatcher: atrapa el restosesiónform loginCSRF
@Bean@Order(1)SecurityFilterChain api(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") // esta cadena solo atiende /api/** .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) // sin cookies de sesión: CSRF no aplica .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); return http.build();}
@Bean@Order(2) // sin securityMatcher: todo lo demásSecurityFilterChain web(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/login").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build();}web.ignoring() frente a permitAll()
Sección titulada «web.ignoring() frente a permitAll()»Verás código antiguo con WebSecurityCustomizer y web.ignoring(). Eso saca la ruta de Spring Security por completo: sin cabeceras de seguridad, sin contexto, sin firewall. La recomendación oficial es usar permitAll() incluso para recursos estáticos: la petición sigue siendo pública, pero conserva las protecciones.
Usuarios para empezar
Sección titulada «Usuarios para empezar»Mientras no tengas base de datos (llegará en la siguiente lección), puedes declarar usuarios en memoria:
@BeanUserDetailsService users(PasswordEncoder encoder) { return new InMemoryUserDetailsManager( User.withUsername("ana").password(encoder.encode("ana-password-123")).roles("USER").build(), User.withUsername("root").password(encoder.encode("root-password-123")).roles("ADMIN").build());}
@BeanPasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder();}Mini lab · 20 min
Dos cadenas
Objetivo: Que /api/** sea stateless con HTTP Basic y que el resto de la aplicación use formulario y sesión.
- Crea
SecurityConfigcon las dos cadenas de arriba y los usuarios en memoria. - Añade
GET /api/admin/statsy protégelo conhasRole("ADMIN")en la cadena de la API (antes deanyRequest()). - Prueba:
curl -i -u ana:ana-password-123 http://localhost:8080/api/admin/statscurl -i -u root:root-password-123 http://localhost:8080/api/admin/stats- Abre
http://localhost:8080/en el navegador.
Preguntas:
- El log de arranque muestra dos
DefaultSecurityFilterChain. ¿Cuál no tieneCsrfFiltery por qué es correcto? anarecibe 403 en/api/admin/stats. ¿Por qué 403 y no 401?- Cambia el
@Order(2)de la cadena web por@Order(0). ¿Qué pasa ahora concurl -u ana:ana-password-123 http://localhost:8080/api/admin/statsy por qué?
Pista
Para la última pregunta, piensa en qué cadena prueba primero FilterChainProxy y qué securityMatcher tiene.
Mostrar respuestaOcultar respuesta
- La cadena de la API, porque la desactivaste con
csrf.disable(). Es correcto porque esa cadena es stateless y se autentica con la cabeceraAuthorization, que el navegador no adjunta solo: no hay cookie de la que un atacante pueda abusar. - Porque
anasí está autenticada (sus credenciales son correctas); lo que no tiene es el rolADMIN.ExceptionTranslationFilterrecibeAccessDeniedExceptioncon un usuario no anónimo y responde 403. Un 401 sería para credenciales ausentes o incorrectas. - La cadena web pasa a evaluarse primero y, como no tiene
securityMatcher, coincide con todo, también con/api/**. La petición decurlya no usa la cadena API: no hay HTTP Basic en la cadena web, así que la cabeceraAuthorizationse ignora,anallega como anónima y recibe un302hacia/login. La cadena API ha dejado de ejecutarse.