La lliçó anterior va tancar el mòdul de dades amb una frase incòmoda: l'API de CicloUrbana està completament oberta. Qualsevol que conegui l'URL pot crear estacions, donar de baixa bicicletes, llegir el correu i el nom dels ciutadans de Ribalta o finalitzar el lloguer d'una altra persona. Mentre tot vivia en memòria era una demostració; ara hi ha una base de dades PostgreSQL amb dades personals reals d'una xarxa municipal i ni una sola comprovació de qui hi ha a l'altre costat del cable.

Aquesta lliçó encara no escriu configuració: construeix el mapa mental sense el qual configurar Spring Security és copiar i enganxar a cegues. Veurem quins problemes resol el framework i per què la seguretat és l'últim lloc on convé improvisar; separarem autenticació, autorització i auditoria; observarem què li passa al projecte en l'instant en què hi afegim una dependència; i entendrem a fons la peça central de tota l'arquitectura, la cadena de filtres, que s'insereix davant del DispatcherServlet que vam conèixer a 03-01. En acabar sabràs anomenar cada objecte que apareix en un stack trace de Spring Security i explicar què fa, que és exactament el que separa qui depura un 403 en cinc minuts de qui es passa la tarda provant anotacions a l'atzar.

Advertiment que travessa tot el mòdul. El que construirem és un punt de partida didàctic, correcte però mínim. Cap configuració de seguretat no hauria d'arribar a producció sense la revisió d'un professional de seguretat i, si el servei és sensible, sense una auditoria externa. I una regla que no admet excepcions: els secrets —contrasenyes, claus de signatura, credencials de base de dades— no es versionen mai al repositori. Tots els valors d'aquest mòdul són ficticis i serveixen només per a l'exemple.

Contingut

  1. Per què la seguretat no s'improvisa
  2. Autenticació, autorització i auditoria
  3. Què passa en afegir spring-boot-starter-security
  4. La cadena de filtres de servlet
  5. Els filtres importants, en ordre
  6. El model d'objectes de Spring Security
  7. El flux d'autenticació genèric
  8. SecurityContextHolder i el ThreadLocal
  9. Mecanismes d'autenticació disponibles
  10. L'OWASP Top 10 aplicat a una API REST
  11. Errors Comuns i Consells
  12. Exercicis

  1. Per què la seguretat no s'improvisa

La reacció natural d'un desenvolupador davant del problema de CicloUrbana és escriure un filtre propi: llegir una capçalera, comparar-la amb una taula, deixar passar o retornar 401. En una tarda funciona. El problema és tot allò que aquest filtre no contempla i que un atacant sí:

  • Emmagatzematge de contrasenyes. Desar MD5(contrasenya) o fins i tot SHA-256(contrasenya) és avui equivalent a desar-les en clar: una GPU domèstica calcula milers de milions de resums SHA-256 per segon. Cal una funció deliberadament lenta i amb sal, com BCrypt, Argon2 o PBKDF2, i saber ajustar-ne el factor de cost.
  • Comparacions en temps constant. Comparar dues cadenes amb equals acaba tan bon punt troba una diferència. Mesurant el temps de resposta amb prou precisió, un atacant pot deduir-ne caràcters. Spring Security compara credencials de manera resistent a aquesta anàlisi.
  • Fixació de sessió. Si l'identificador de sessió no es regenera en iniciar la sessió, un atacant que aconsegueixi plantar un identificador conegut al navegador de la víctima n'hereta la sessió autenticada.
  • CSRF. Un formulari en un lloc maliciós pot provocar que el navegador d'un usuari autenticat enviï una petició legítima —amb les seves galetes— a CicloUrbana.
  • Enumeració d'usuaris. Respondre «aquest correu no existeix» i «contrasenya incorrecta» amb missatges diferents regala a l'atacant la llista de correus vàlids.
  • Ordre de les regles, rutes equivalents, codificacions alternatives. /api/v1/Estacions, /api/v1//estacions, /api/v1/estacions;jsessionid=x i /api/v1/%65stacions poden arribar al mateix controlador i esquivar una comprovació ingènua basada en startsWith.

Spring Security és la resposta a vint anys d'aquests errors comesos en públic. És una biblioteca madura, amb divulgació responsable de vulnerabilitats, versions apedaçades i un model que separa clarament responsabilitats. La regla professional és simple: no escriguis criptografia ni mecanismes d'autenticació propis; configura els que ja estan auditats.

  1. Autenticació, autorització i auditoria

Tres conceptes que el llenguatge col·loquial confon i que en el codi són tres capes diferents.

Concepte Pregunta que respon Quan passa A Spring Security Exemple a CicloUrbana
Autenticació (AuthN) Qui ets? Al principi de la petició AuthenticationManager, AuthenticationProvider La Marta presenta el seu correu [email protected] i la seva contrasenya; el sistema confirma que és ella
Autorització (AuthZ) Pots fer això? Abans d'executar l'operació AuthorizationManager, AuthorizationFilter, @PreAuthorize La Marta és CIUTADA: pot llogar, però no pot crear l'estació "Plaça Major"
Auditoria Qui ha fet què i quan? Després, i de manera permanent Esdeveniments (AuthenticationSuccessEvent), EntitatAuditable de 04-03 Queda registrat que l'operari [email protected] va marcar RB-0142 com a avariada el 12 de març a les 09:14

Fallen de maneres diferents i amb codis HTTP diferents, i confondre'ls és l'error més comú del mòdul:

Situació Codi HTTP Significat literal
No hi ha credencials, o són invàlides 401 Unauthorized «No sé qui ets. Autentica't.»
Credencials vàlides, permisos insuficients 403 Forbidden «Sé qui ets, i no pots.»
Recurs inexistent o existent però aliè 404 Not Found «Aquí no hi ha res» (de vegades preferible a un 403 que confirma l'existència)

El nom 401 Unauthorized és un error històric de l'especificació: significa no autenticat. La capçalera que l'acompanya, WWW-Authenticate, deixa clar que parla d'autenticació.

Una quarta peça apareix constantment i convé anomenar-la: la identificació. La Marta s'identifica dient que és [email protected] —això és una dada pública— i s'autentica demostrant-ho amb alguna cosa que només ella sap. El correu identifica; la contrasenya autentica.

  1. Què passa en afegir spring-boot-starter-security

La manera més ràpida d'entendre el framework és observar l'efecte d'una sola línia al pom.xml. Partim del projecte tal com va quedar a 04-08.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Sense versió, perquè el spring-boot-starter-parent de 01-04 la gestiona: amb Spring Boot 3.x correspon a Spring Security 6.x. En arrencar, el log mostra una cosa que abans no hi era:

Using generated security password: 8f2a4c31-5b7e-4d19-9c02-6ea3f1b0d47c

This generated password is for development use only.
Your security configuration must be updated before running your application in production.

Aquest identificador és aleatori a cada arrencada i només existeix en desenvolupament (ho comprovarem a l'exercici 1). Ara provem l'API amb curl, exactament com a 03-02:

curl -i http://localhost:8080/api/v1/estacions
HTTP/1.1 401
WWW-Authenticate: Basic realm="Realm"
Set-Cookie: JSESSIONID=6C0F...; Path=/; HttpOnly
Content-Type: application/json
{"timestamp":"2026-03-12T09:14:22.881+00:00","status":401,
 "error":"Unauthorized","path":"/api/v1/estacions"}

Ha canviat tot sense escriure ni una línia de codi. L'autoconfiguració de 02-06 ha detectat la dependència i ha aplicat la seva decisió per defecte, que és l'única defensa raonable: tot protegit. Val la pena enumerar què s'ha activat exactament, perquè cada punt es pot modificar i ho farem a 05-02:

Comportament per defecte Detall
Totes les rutes requereixen autenticació Inclosos /api/v1/**, /swagger-ui.html i /actuator/**
Un usuari en memòria Nom user, contrasenya generada al log
HTTP Basic activat D'aquí la capçalera WWW-Authenticate: Basic
Formulari d'inici de sessió activat A /login, generat pel mateix framework
Sessió HTTP creada La galeta JSESSIONID de la resposta
CSRF activat POST, PUT, PATCH i DELETE exigeixen un token
Capçaleres de seguretat afegides X-Content-Type-Options, X-Frame-Options, Cache-Control
Consola H2 i recursos estàtics També protegits

Amb les credencials per defecte la petició torna a funcionar:

curl -i -u user:8f2a4c31-5b7e-4d19-9c02-6ea3f1b0d47c \
     http://localhost:8080/api/v1/estacions
# HTTP/1.1 200  →  les quatre estacions de Ribalta

L'opció -u de curl construeix la capçalera Authorization: Basic dXNlcjo4ZjJhNGMzMS0..., que és simplement usuari:contrasenya codificat en Base64. Base64 no és xifratge: qualsevol el pot descodificar amb base64 -d. Per això HTTP Basic només és admissible sobre HTTPS, un punt al qual tornarem a 05-05.

I si obres http://localhost:8080/api/v1/estacions en un navegador, no veuràs el 401: veuràs un formulari d'inici de sessió. La diferència és a la capçalera Accept que envia el navegador (text/html), que Spring Security fa servir per triar entre respondre amb el formulari o amb el repte HTTP Basic. Aquest formulari és una pàgina HTML que genera el mateix framework; no existeix cap fitxer al projecte.

Cap d'aquests valors per defecte no serveix per a CicloUrbana: un usuari anomenat user amb una contrasenya que canvia a cada reinici no és un model d'usuaris, i un formulari HTML no li serveix a una aplicació mòbil que consumeix JSON. Però són una bastida deliberada: el projecte queda segur per defecte i trencat de manera visible, que és infinitament millor que quedar obert de manera silenciosa.

Què NO fa per tu aquesta dependència

Convé marcar el límit des del principi, perquè la comoditat de l'autoconfiguració indueix a pensar que la feina està feta:

  • No sap qui són els teus usuaris. L'usuari user és un substitut temporal; connectar la taula usuaris de Ribalta és feina teva (05-03).
  • No coneix els teus rols ni les teves regles de negoci. «Un ciutadà només finalitza els seus lloguers» és una frase sobre el domini de CicloUrbana que cap framework no pot endevinar.
  • No xifra el transport. Sense HTTPS, la capçalera Authorization viatja llegible per la xarxa (05-05).
  • No valida les dades d'entrada. Això continua sent Bean Validation (03-04).
  • No et protegeix de les teves pròpies consultes. Si concatenes cadenes en una consulta nativa, la injecció SQL continua sent possible (04-06).

  1. La cadena de filtres de servlet

Per entendre on passa tot això cal reprendre el DispatcherServlet de 03-01. Una aplicació web Java es recolza en l'API de Servlet, que defineix dues peces: els servlets, que atenen peticions, i els filtres, que les embolcallen formant una cadena per la qual la petició passa abans d'arribar al servlet i per la qual la resposta torna després.

Spring Security és, en essència, un filtre. No toca el DispatcherServlet, no modifica els teus controladors i no depèn de Spring MVC: se situa davant de tot i decideix si la petició continua avançant.

flowchart LR
    C["Client<br/>app mòbil / curl"] --> T["Contenidor de servlets<br/>Tomcat"]
    T --> DFP["DelegatingFilterProxy<br/>springSecurityFilterChain"]
    DFP --> FCP["FilterChainProxy<br/>bean de Spring"]
    FCP --> SFC["SecurityFilterChain<br/>llista ordenada de filtres"]
    SFC --> DS["DispatcherServlet<br/>lliçó 03-01"]
    DS --> CTRL["EstacioController<br/>EstacioService"]

Tres noms que apareixen sens falta en qualsevol traça d'error i que convé distingir:

DelegatingFilterProxy. Un filtre estàndard de l'API de Servlet, registrat al contenidor (Tomcat). L'única feina que fa és delegar en un bean de Spring anomenat springSecurityFilterChain. Existeix perquè Tomcat no coneix el contenidor de Spring: instancia filtres per la seva classe, sense injecció de dependències ni cicle de vida de beans. DelegatingFilterProxy és el pont entre els dos mons, i gràcies a ell els filtres de seguretat són beans normals que poden injectar UsuariRepositori com qualsevol servei.

FilterChainProxy. El bean en què delega. És el punt d'entrada únic de Spring Security, i la seva feina és triar quina cadena s'aplica a aquesta petició concreta. N'hi pot haver diverses.

SecurityFilterChain. Una parella formada per un criteri de coincidència (RequestMatcher) i una llista ordenada de filtres. FilterChainProxy recorre les cadenes registrades en ordre, es queda amb la primera el criteri de la qual coincideix i n'executa els filtres. Les altres ni tan sols es consulten. Aquest detall causa moltes confusions quan hi ha diverses cadenes, i el tractarem a 05-02.

flowchart TD
    R["Petició: GET /api/v1/estacions"] --> FCP["FilterChainProxy"]
    FCP --> M1{"Coincideix amb<br/>/actuator/**?"}
    M1 -- "No" --> M2{"Coincideix amb<br/>/api/**?"}
    M1 -- "Sí" --> CH1["Cadena 1 · @Order(1)"]
    M2 -- "Sí" --> CH2["Cadena 2 · @Order(2)"]
    M2 -- "No" --> CH3["Cadena per defecte"]
    CH2 --> F["Filtres: context → CORS → autenticació → autorització"]
    F --> DS["DispatcherServlet"]

Una conseqüència que sorprèn: els filtres de seguretat s'executen abans que el @RestControllerAdvice de 03-06. Quan la fallada és d'autenticació o autorització, GestorGlobalExcepcions no se n'assabenta, perquè l'excepció es produeix fora de l'abast del DispatcherServlet. Aquest és el motiu que la resposta 401 de més amunt no tingui el format ProblemDetail que tant vam cuidar a 03-06, sinó l'error genèric de Spring Boot. Ho arreglarem a 05-03 amb un AuthenticationEntryPoint propi. De la mateixa manera, FiltreRastreig —també un OncePerRequestFilter— només aporta el seu identificador de rastreig als esdeveniments de seguretat si es registra a la posició adequada de la cadena.

  1. Els filtres importants, en ordre

Una cadena típica té una quinzena de filtres. El seu ordre és fix i està definit a FilterOrderRegistration; no es tria lliurement, tot i que sí que s'hi poden inserir filtres propis en posicions relatives (addFilterBefore, addFilterAfter), com farem amb el filtre JWT a 05-04. Aquests són els que cal conèixer:

# Filtre Què fa
1 DisableEncodeUrlFilter Impedeix que el contenidor afegeixi el jsessionid als URL, i evita que es filtri en logs i enllaços
2 SecurityContextHolderFilter Carrega el SecurityContext de la sessió en començar i el neteja en acabar. A Spring Security 6 ja no el desa automàticament
3 HeaderWriterFilter Escriu les capçaleres de seguretat de la resposta (X-Frame-Options, etc.)
4 CorsFilter Aplica la política CORS. Ha d'anar abans de l'autorització perquè les peticions OPTIONS de sondeig no requereixin credencials
5 CsrfFilter Verifica el token anti-CSRF a les peticions que modifiquen estat
6 LogoutFilter Intercepta /logout, neteja el context i invalida la sessió
7 UsernamePasswordAuthenticationFilter Processa l'enviament del formulari d'inici de sessió (POST /login)
8 BasicAuthenticationFilter Llegeix la capçalera Authorization: Basic
9 BearerTokenAuthenticationFilter Llegeix Authorization: Bearer quan es fa servir OAuth2 Resource Server. A 05-04 escriurem el nostre equivalent, FiltreAutenticacioJwt
10 RequestCacheAwareFilter Recupera la petició original desada abans de redirigir a l'inici de sessió
11 AnonymousAuthenticationFilter Si ningú no s'ha autenticat, col·loca un Authentication anònim. Mai no hi ha null al context
12 ExceptionTranslationFilter Captura AuthenticationException i AccessDeniedException del filtre següent i les tradueix a 401 o 403
13 AuthorizationFilter L'últim. Consulta les regles d'authorizeHttpRequests i decideix si la petició passa

Aquesta llista no cal memoritzar-la: es pot imprimir. Amb el nivell de log adequat, Spring Security escriu en arrencar la cadena completa en l'ordre real de l'aplicació, que és la referència definitiva quan alguna cosa no quadra:

logging:
  level:
    org.springframework.security.web.FilterChainProxy: DEBUG
Will secure any request with [
  org.springframework.security.web.session.DisableEncodeUrlFilter,
  org.springframework.security.web.context.SecurityContextHolderFilter,
  org.springframework.security.web.header.HeaderWriterFilter,
  org.springframework.web.filter.CorsFilter,
  org.springframework.security.web.csrf.CsrfFilter,
  ...
  org.springframework.security.web.access.ExceptionTranslationFilter,
  org.springframework.security.web.access.intercept.AuthorizationFilter ]

Tres observacions que eviten errors molt cars:

AnonymousAuthenticationFilter explica per què SecurityContextHolder.getContext().getAuthentication() gairebé mai no retorna null. Retorna un AnonymousAuthenticationToken amb l'autoritat ROLE_ANONYMOUS. Comprovar if (auth != null) per saber si hi ha usuari és un error clàssic: cal comprovar auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken), o fer servir directament l'expressió authenticated de la configuració.

ExceptionTranslationFilter va just abans d'AuthorizationFilter, i aquest ordre és intencionat. Embolcalla l'últim filtre en un try/catch: quan l'autorització rebutja la petició, l'excepció puja i aquest filtre decideix. Si l'usuari és anònim, invoca l'AuthenticationEntryPoint → 401. Si ja estava autenticat, invoca l'AccessDeniedHandler → 403. Aquesta és tota la lògica que distingeix un 401 d'un 403 a Spring Security, i per això personalitzar tots dos objectes (05-03) és el que integra la seguretat amb el nostre ProblemDetail.

SecurityContextHolderFilter neteja el context al seu bloc finally. El motiu el veurem a l'apartat 8 i és imprescindible entendre'l.

  1. El model d'objectes de Spring Security

Nou tipus que apareixen una vegada i una altra. Aprendre'ls ara estalvia hores després.

Tipus Què és Analogia a CicloUrbana
Authentication L'objecte central: representa una petició d'autenticació o el resultat. Conté principal, credentials, authorities i isAuthenticated() El carnet de la Marta, abans i després de validar-lo
Principal La identitat. Abans d'autenticar sol ser el String del correu; després, un UserDetails «Marta Aguiló, [email protected]»
GrantedAuthority Un permís concret, gairebé sempre una cadena. Amb el prefix ROLE_ representa un rol ROLE_CIUTADA, ROLE_OPERARI, ROLE_ADMIN
SecurityContext Un contenidor amb un Authentication a dins La fitxa de la petició en curs
SecurityContextHolder El magatzem estàtic que dona accés al SecurityContext del fil actual El taulell on es consulta aquesta fitxa
AuthenticationManager La porta d'entrada: rep un Authentication sense validar i en retorna un de validat o llança una excepció El cap de l'oficina d'atenció
AuthenticationProvider Cada mecanisme concret de validació. ProviderManager els prova en ordre La finestreta que sap validar carnets amb contrasenya
UserDetails El que el sistema sap d'un usuari: nom, resum de contrasenya, autoritats, si està actiu o bloquejat La fitxa de la Marta a la base de dades
UserDetailsService L'única funció que carrega un UserDetails a partir del seu nom L'arxiu on es busca aquesta fitxa

Val la pena veure les signatures reals, perquè són sorprenentment petites:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();   // la contrasenya; s'esborra després d'autenticar
    Object getDetails();       // IP, identificador de sessió...
    Object getPrincipal();     // l'usuari: String o UserDetails
    boolean isAuthenticated();
    void setAuthenticated(boolean autenticat) throws IllegalArgumentException;
}

@FunctionalInterface
public interface AuthenticationManager {
    Authentication authenticate(Authentication autenticacio) throws AuthenticationException;
}

public interface UserDetailsService {
    UserDetails loadUserByUsername(String nomUsuari) throws UsernameNotFoundException;
}

I la fitxa de l'usuari, que implementarem amb una classe pròpia a 05-03:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();                 // el RESUM, mai la contrasenya en clar
    String getUsername();                 // a CicloUrbana serà el correu
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();         // bloqueig per intents fallits
    boolean isCredentialsNonExpired();    // caducitat de contrasenya
    boolean isEnabled();                  // el camp `actiu` de la taula usuaris
}

Els quatre mètodes booleans són quatre motius diferents pels quals un usuari existent pot no poder entrar, i Spring Security els distingeix amb excepcions diferents (AccountExpiredException, LockedException, CredentialsExpiredException, DisabledException). Si no en necessites algun, retorna true, però fes-ho conscientment: retornar true a isEnabled() deixa entrar els usuaris donats de baixa.

Tres detalls que revelen el disseny. AuthenticationManager rep i retorna el mateix tipus: hi entra un objecte «pretenc ser la Marta amb aquesta contrasenya» i en surt un objecte «soc la Marta i tinc aquestes autoritats». UserDetailsService no valida res: només busca i retorna; qui compara la contrasenya és l'AuthenticationProvider. I getCredentials() retorna Object perquè unes vegades és una contrasenya, altres un token i altres un certificat.

UserDetailsService és a més el punt d'extensió que farem servir a 05-03: implementar-lo contra UsuariRepositori és tot el que cal perquè Spring Security autentiqui contra la taula usuaris de Ribalta.

  1. El flux d'autenticació genèric

Amb els noms clars, el flux complet d'un inici de sessió amb usuari i contrasenya:

sequenceDiagram
    participant C as Client
    participant F as Filtre d'autenticació
    participant AM as AuthenticationManager<br/>(ProviderManager)
    participant AP as DaoAuthenticationProvider
    participant UDS as UserDetailsService
    participant PE as PasswordEncoder
    participant SCH as SecurityContextHolder

    C->>F: Credencials (Basic, formulari o JSON)
    F->>F: Construeix UsernamePasswordAuthenticationToken<br/>(sense autenticar)
    F->>AM: authenticate(token)
    AM->>AP: Admets aquest tipus de token?
    AP->>UDS: loadUserByUsername("[email protected]")
    UDS-->>AP: UserDetails (resum + autoritats + actiu)
    AP->>PE: matches(contrasenyaPlana, resumEmmagatzemat)
    PE-->>AP: true
    AP-->>AM: Authentication AUTENTICAT<br/>credencials esborrades
    AM-->>F: Authentication autenticat
    F->>SCH: setContext(context amb l'Authentication)
    F->>C: Continua la cadena → controlador

Cinc punts que convé retenir:

  1. El filtre no valida res. Només extreu credencials del transport (capçalera, formulari o JSON) i construeix un Authentication sense autenticar. Per això hi ha un filtre diferent per mecanisme i tots acaben al mateix AuthenticationManager.
  2. ProviderManager és la implementació habitual d'AuthenticationManager i conté una llista d'AuthenticationProvider. Pregunta a cadascun si admet el tipus de token, i el primer que respongui que sí decideix. Això permet conviure diversos mecanismes —base de dades, LDAP, JWT— a la mateixa aplicació.
  3. La comparació de contrasenyes passa a l'AuthenticationProvider, amb el PasswordEncoder, no al UserDetailsService.
  4. L'Authentication retornat és un objecte nou, autenticat i amb les credencials esborrades: la contrasenya en clar no ha de sobreviure en memòria més del que és imprescindible.
  5. Desar el resultat al SecurityContextHolder és responsabilitat del filtre, no del gestor. És exactament el que farà el nostre FiltreAutenticacioJwt a 05-04.

  1. SecurityContextHolder i el ThreadLocal

El SecurityContextHolder és el punt des del qual qualsevol codi de l'aplicació —un servei, un aspecte, un @PreAuthorize— esbrina qui està fent la petició:

Authentication autenticacio = SecurityContextHolder.getContext().getAuthentication();
String correu = autenticacio.getName();                     // "[email protected]"
boolean esOperari = autenticacio.getAuthorities().stream()
        .anyMatch(a -> a.getAuthority().equals("ROLE_OPERARI"));

La seva estratègia d'emmagatzematge per defecte és MODE_THREADLOCAL: el context es desa en una variable lligada al fil que atén la petició. És una decisió elegant —permet consultar l'usuari en qualsevol punt sense arrossegar-lo com a paràmetre— amb tres conseqüències que cal conèixer.

Primera: el context s'ha de netejar sempre. Els contenidors de servlets reutilitzen fils mitjançant un pool. Si en acabar la petició de la Marta el context hi continués, la petició següent atesa per aquest mateix fil —potser d'un usuari anònim— heretaria la identitat de la Marta. És una vulnerabilitat greu de suplantació. Per això SecurityContextHolderFilter neteja en un finally, i per això un filtre propi que cridi SecurityContextHolder.setContext(...) també ha de netejar, o delegar en el filtre estàndard. És el mateix problema que el MDC.remove() de FiltreRastreig a 03-06, amb conseqüències molt pitjors.

Segona: el context no es propaga a altres fils. Si un servei llança una tasca amb @Async o amb un ExecutorService, aquest fil nou té un context buit, i qualsevol @PreAuthorize que hi hagi allà veurà un usuari anònim:

@Service
public class ServeiNotificacions {

    @Async   // compte! fil diferent: el SecurityContext NO hi viatja
    public void avisarBateriaBaixa(Long idBicicleta) {
        var auth = SecurityContextHolder.getContext().getAuthentication();
        // auth és el token anònim, no l'operari que va disparar l'operació
    }
}

Les solucions estàndard són DelegatingSecurityContextExecutor, DelegatingSecurityContextRunnable o l'estratègia MODE_INHERITABLETHREADLOCAL. Com que CicloUrbana encara no té tasques asíncrones, deixem l'assumpte anotat: l'asincronia i la seva interacció amb el context de seguretat es tracten a 07-03.

Tercera: a Spring Security 6 el context ja no es desa sol. A la versió 5, SecurityContextPersistenceFilter desava el context a la sessió automàticament en acabar. A la 6, SecurityContextHolderFilter només llegeix; desar és una acció explícita mitjançant SecurityContextRepository. El canvi buscava evitar sessions creades sense voler, i és una font habitual de migracions trencades. Per a CicloUrbana és indiferent: l'API serà sense estat i no hi haurà sessió per desar.

  1. Mecanismes d'autenticació disponibles

Spring Security no imposa cap mecanisme; n'ofereix molts sobre el mateix model d'objectes.

Mecanisme Com viatja la credencial Amb estat Encaixa bé a Limitacions
Form login POST /login + galeta de sessió Sí Aplicacions web amb vistes al servidor Inútil per a una API JSON; arrossega CSRF i sessió
HTTP Basic Authorization: Basic base64(u:p) No Proves, eines internes Envia la contrasenya a cada petició; exigeix HTTPS
JWT / OAuth2 Resource Server Authorization: Bearer <token> No API REST i apps mòbils Revocació difícil; cal gestionar la caducitat
OAuth2 / OIDC Client Redirecció a un proveïdor extern Depèn «Entra amb Google», corporatiu Requereix un proveïdor d'identitat
SAML 2.0 Assercions XML signades Sí Integració amb identitat corporativa clàssica Complex, orientat a navegador
LDAP / Active Directory Usuari i contrasenya contra el directori Indiferent Empreses amb directori central Necessita el directori operatiu
Certificats de client (mTLS) Certificat X.509 en l'handshake TLS No Comunicació màquina a màquina Gestió de certificats costosa
API keys Capçalera pròpia No Integracions de servidor a servidor No identifica persones; rotació manual

L'elecció de CicloUrbana és JWT, i convé justificar-la amb els requisits concrets del projecte:

  • El client principal és una aplicació mòbil, que no té navegador ni gestiona galetes de manera natural.
  • L'API és REST i sense estat (03-01): una sessió al servidor contradiu aquesta restricció i complica escalar horitzontalment a diverses instàncies, una cosa que importarà al mòdul 7.
  • L'Ajuntament de Ribalta no disposa d'un proveïdor d'identitat corporatiu, així que OIDC i SAML afegirien una peça d'infraestructura sense aportar valor avui.
  • Un Bearer l'entén qualsevol client: l'app mòbil, el tauler web, curl i el mateix Swagger UI de 03-07.

Una precisió important: triar un mecanisme no és triar una única línia de defensa. La seguretat es construeix per capes —TLS al transport, autenticació a l'entrada, autorització per URL, autorització per mètode, validació de dades i restriccions a la base de dades—, de manera que la fallada d'una no comprometi el sistema sencer. L'índex únic parcial uk_lloguers_usuari_en_curs de 04-08 n'és un bon exemple: encara que una fallada d'autorització permetés iniciar un lloguer indegut, el motor continuaria impedint que un usuari tingui dos lloguers simultanis.

Tot això s'implementa a 05-04. I allà veurem també l'alternativa madura per a producció, spring-boot-starter-oauth2-resource-server amb un proveïdor extern com Keycloak, que evita escriure codi d'emissió de tokens.

  1. L'OWASP Top 10 aplicat a una API REST

L'OWASP Top 10 és la llista de referència dels riscos de seguretat més crítics en aplicacions web, publicada per l'Open Worldwide Application Security Project. Repassar-la amb CicloUrbana al cap aclareix què resol el framework i què continua sent responsabilitat del programador. Aquesta distinció és la lliçó més important del mòdul.

Risc OWASP Què significa a CicloUrbana Què aporta Spring Security Què et toca a tu
A01 Control d'accés trencat Un ciutadà finalitza el lloguer d'un altre canviant l'id de l'URL Regles per URL, @PreAuthorize, AuthorizationFilter Escriure les regles correctes i comprovar la propietat del recurs (05-05)
A02 Fallades criptogràfiques Contrasenyes en clar; API per HTTP sense xifrar PasswordEncoder amb BCrypt, HSTS, requiresChannel Terminar TLS bé, no inventar xifratge, rotar claus
A03 Injecció ... WHERE correu = ' + entrada + ' Res directament Consultes parametritzades: JPA i @Query de 04-06 ja ho fan
A04 Disseny insegur No limitar els lloguers simultanis per usuari Res Modelar bé les regles de negoci (l'índex únic parcial de 04-08 n'és un exemple)
A05 Configuració incorrecta Swagger UI o la consola H2 obertes en producció Valors per defecte segurs i capçaleres Revisar perfils (07-02) i tancar el que no s'ha d'exposar
A06 Components vulnerables Una versió antiga d'una llibreria amb CVE Les seves pròpies versions apedaçades Actualitzar i auditar dependències (05-05)
A07 Fallades d'identificació i autenticació Contrasenyes febles, sense bloqueig de compte Mecanismes robustos i protecció de sessió Política de contrasenyes, límit d'intents, segon factor
A08 Fallades d'integritat Acceptar un JWT sense verificar-ne la signatura Verificació de signatura, protecció CSRF No deserialitzar dades no fiables
A09 Fallades de registre i monitoratge Ningú no detecta 10.000 intents d'inici de sessió fallits Esdeveniments d'autenticació publicats Registrar-los, alertar i no registrar mai contrasenyes ni tokens
A10 SSRF Un endpoint que descarrega un URL indicat pel client Res Validar i limitar destinacions de sortida

La conclusió és doble i molt pràctica. Spring Security no et fa segur: et dona eines correctes. Dels deu riscos, el framework en cobreix parcialment quatre, ajuda en tres i no intervé en tres. I els dos riscos on més aporta —A01 i A07— són precisament on és més fàcil equivocar-se, perquè una regla mal escrita es veu igual que una de ben escrita: l'aplicació arrenca, respon 200 i ningú no nota res fins que algú mira el que no havia de mirar.

Existeix a més un OWASP API Security Top 10 específic per a API, els dos primers riscos del qual són BOLA (autorització trencada a nivell d'objecte: accedir al recurs d'un altre canviant un identificador) i l'autenticació trencada. Tots dos són exactament els problemes que resoldrem a 05-03 i 05-05.

Errors Comuns i Consells

Creure que treure l'enllaç del frontend protegeix un endpoint. Si DELETE /api/v1/estacions/1 funciona sense credencials, tant se val que cap botó no el cridi: curl existeix. La seguretat s'aplica sempre al servidor.

Escriure un filtre d'autenticació propi «perquè és més simple». Ho és fins al primer incident. Tot el que hem enumerat a l'apartat 1 —temporització, fixació de sessió, enumeració d'usuaris, normalització de rutes— s'ha de resoldre, i ja està resolt.

Confondre 401 amb 403. Retornar 403 a qui no ha presentat credencials impedeix que un client sàpiga que s'ha d'autenticar; retornar 401 a qui sí que està autenticat el farà reintentar en bucle.

Confiar en l'ofuscació. Base64 no és xifratge, un identificador llarg a l'URL no és cap secret i un endpoint «que ningú no coneix» apareix als logs, a l'historial del navegador i a l'escaneig automàtic pocs dies després de publicar-se.

Comprovar authentication != null per saber si hi ha usuari. AnonymousAuthenticationFilter garanteix que gairebé mai no és null. Comprova autenticació real o, millor, delega en les regles del framework.

Deixar la contrasenya generada al log d'un entorn compartit. Està pensada exclusivament per a desenvolupament local, i el mateix missatge ho adverteix. Qualsevol amb accés als logs de preproducció hi entra.

Consell: activa el log de depuració des del primer dia. Amb logging.level.org.springframework.security: DEBUG veuràs quins filtres s'executen i quina regla rebutja la petició. És la diferència entre depurar i endevinar. Detalls a 05-02.

Consell: dibuixa la teva cadena de filtres. Quan alguna cosa no funciona, la pregunta útil gairebé mai no és «quina anotació falta?», sinó «en quin filtre s'ha aturat la petició?».

Consell: la seguretat té data de caducitat. Una configuració correcta avui pot no ser-ho d'aquí a dos anys. Programa revisions periòdiques i actualitzacions de dependències com a part del manteniment.

Exercicis

Exercici 1

Afegeix spring-boot-starter-security al projecte CicloUrbana i, sense escriure configuració, respon amb curl i amb el log:

  1. Què retorna GET /api/v1/estacions sense credencials? I amb les credencials del log?
  2. Què retorna POST /api/v1/estacions amb les credencials correctes i un cos vàlid? Explica el resultat.
  3. Continua accessible /swagger-ui.html?
  4. Què passa si defineixes spring.security.user.name i spring.security.user.password a application.yml? Desapareix el missatge del log? Per què no s'ha de fer així en un projecte real?

Exercici 2

Classifica cada situació de CicloUrbana com a fallada d'autenticació (401), d'autorització (403) o de cap de les dues, i indica quin component de Spring Security hi intervé:

# Situació
a Un client crida GET /api/v1/lloguers/7 sense capçalera Authorization
b La ciutadana Marta crida DELETE /api/v1/estacions/1
c L'operari Lluís crida PATCH /api/v1/bicicletes/RB-0142 amb un estat inexistent
d Un client presenta un token caducat
e La Marta crida POST /api/v1/lloguers/9/finalitzar, i el lloguer 9 és d'un altre ciutadà
f Un client crida GET /api/v1/estacions/99, que no existeix

Exercici 3

Descriu, filtre a filtre, el recorregut complet d'aquesta petició al CicloUrbana actual (amb la dependència afegida i sense configuració pròpia), indicant què fa cada filtre rellevant i on i per què s'atura:

curl -i -X POST http://localhost:8080/api/v1/estacions \
     -H 'Content-Type: application/json' \
     -d '{"nom":"Mercat Vell","capacitat":20}'

Solucions

Solució 1

1. Sense credencials, 401 amb WWW-Authenticate: Basic realm="Realm" i un cos d'error genèric de Spring Boot —no un ProblemDetail—, perquè el rebuig passa a ExceptionTranslationFilter, abans que la petició arribi al DispatcherServlet i per tant fora de l'abast de GestorGlobalExcepcions (03-06). Amb -u user:<contrasenya del log>, 200 i el llistat de les quatre estacions de Ribalta.

2. Retorna 403 Forbidden, i desconcerta perquè les credencials són correctes. El culpable és el CSRF: està activat per defecte i CsrfFilter rebutja tot POST, PUT, PATCH i DELETE que no porti un token vàlid. No és un problema de permisos, tot i que el codi ho sembli. La confirmació és al log amb DEBUG activat:

o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for
http://localhost:8080/api/v1/estacions

A 05-02 desactivarem el CSRF justificant per què és segur fer-ho en una API sense estat amb tokens.

3. No. /swagger-ui.html i /v3/api-docs queden protegits com tota la resta: al navegador apareix el formulari d'inici de sessió. A 05-02 s'obriran només en desenvolupament.

4. Definint les dues propietats, l'usuari en memòria passa a tenir aquest nom i aquesta contrasenya, i el missatge del log desapareix: només es genera quan la contrasenya no està configurada. No serveix per a un projecte real per tres motius: és un únic usuari sense rols ni identitat, així que no hi pot haver ciutadans, operaris i administradors; la contrasenya està en clar en un fitxer versionat a Git, exactament el que 02-04 va prohibir en parlar de secrets; i no hi ha manera de donar d'alta usuaris sense reiniciar. La solució arriba a 05-03 amb UserDetailsService contra la taula usuaris.

Solució 2

# Tipus Codi Component
a Autenticació 401 ExceptionTranslationFilter detecta usuari anònim i invoca l'AuthenticationEntryPoint
b Autorització 403 AuthorizationFilter: la Marta està autenticada però no té ROLE_ADMIN
c Cap 400 És validació d'entrada (03-04). La seguretat ja l'ha deixat passar; respon GestorGlobalExcepcions
d Autenticació 401 El filtre de token rebutja la credencial: és invàlida, no insuficient
e Autorització 403 Cap filtre per URL no ho pot resoldre: la ruta és legítima i el permís depèn de la dada. Requereix seguretat de mètode (05-05)
f Cap 404 RecursNoTrobatException de 03-06

Els casos c i f ensenyen que no tot error és de seguretat. El cas e és el més important del mòdul: és el risc A01/BOLA de l'apartat 10 i demostra per què les regles per URL no basten.

Solució 3

  1. DelegatingFilterProxy rep la petició de Tomcat i delega en el bean springSecurityFilterChain.
  2. FilterChainProxy busca la primera SecurityFilterChain que coincideix. Només hi ha la de per defecte, que s'aplica a /**.
  3. SecurityContextHolderFilter intenta carregar un SecurityContext de la sessió. No hi ha galeta JSESSIONID, així que el context queda buit.
  4. HeaderWriterFilter prepara les capçaleres de seguretat de la resposta.
  5. CsrfFilter s'activa perquè POST sí que modifica estat. Busca el token anti-CSRF al paràmetre _csrf o a la capçalera X-CSRF-TOKEN. No el troba i llança InvalidCsrfTokenException (o MissingCsrfTokenException).
  6. ExceptionTranslationFilter, que embolcalla els següents, no arriba a intervenir en aquest cas: CsrfFilter és abans que ell a la cadena i resol la resposta amb el seu propi AccessDeniedHandler.
  7. La resposta és 403 Forbidden. BasicAuthenticationFilter, AuthorizationFilter, el DispatcherServlet, EstacioController i EstacioService no s'executen mai.

La moralitat és l'objectiu d'aquesta lliçó: la petició va morir a cinc filtres de distància del teu codi, i cap anotació al controlador no ho hauria canviat. Sense el mapa de la cadena, aquest 403 sembla un problema de permisos i s'hi perd una tarda sencera.

Conclusió

Ja tens el mapa. Saps per què la seguretat no s'improvisa —emmagatzematge de contrasenyes, comparacions en temps constant, fixació de sessió, CSRF, enumeració d'usuaris i normalització de rutes són problemes resolts que ningú no hauria de reescriure— i distingeixes amb precisió autenticació (qui ets, 401), autorització (què pots fer, 403) i auditoria (què vas fer). Has vist l'efecte immediat d'afegir spring-boot-starter-security: tot protegit, un usuari user amb contrasenya generada al log, HTTP Basic i formulari activats, sessió, CSRF i capçaleres de seguretat; i has comprovat amb curl que GET /api/v1/estacions respon 401 i que un POST amb credencials correctes respon 403 per CSRF, un desconcert que ara saps explicar.

Sobretot, entens l'arquitectura. DelegatingFilterProxy estén el pont entre Tomcat i el contenidor de Spring; FilterChainProxy tria la primera SecurityFilterChain que coincideix; i aquesta cadena executa els seus filtres en un ordre fix abans que la petició arribi al DispatcherServlet de 03-01. Coneixes el paper de SecurityContextHolderFilter, CorsFilter, CsrfFilter, els filtres d'autenticació, AnonymousAuthenticationFilter, ExceptionTranslationFilter —el que decideix entre 401 i 403— i AuthorizationFilter. I saps la conseqüència pràctica més útil: quan la seguretat rebutja una petició, el teu @RestControllerAdvice de 03-06 ni se n'assabenta, perquè el rebuig passa fora de l'abast del DispatcherServlet.

Tens també el vocabulari: Authentication, Principal, GrantedAuthority, SecurityContext, SecurityContextHolder, AuthenticationManager, AuthenticationProvider, UserDetails i UserDetailsService, amb el flux que els uneix i el detall del ThreadLocal que obliga a netejar el context a cada petició i que impedeix que la identitat viatgi sola a un fil @Async —un cap que reprendrem a 07-03—. Has comparat els mecanismes d'autenticació disponibles i saps per què CicloUrbana tria JWT: client mòbil, API sense estat i absència d'un proveïdor d'identitat corporatiu. I has situat l'OWASP Top 10 sobre el projecte, amb la conclusió que governa tot el mòdul: Spring Security dona eines correctes, no seguretat automàtica, i els riscos on més aporta són també on és més fàcil equivocar-se.

La lliçó següent deixa la teoria i escriu la primera classe real del paquet com.ciclourbana.seguretat. A 05-02, Configuració de Spring Security, crearem ConfiguracioSeguretat amb el seu bean SecurityFilterChain i el DSL de lambdes de Spring Security 6; definirem amb authorizeHttpRequests i requestMatchers el mapa complet d'accessos de Ribalta —estacions públiques, lloguers autenticats, bicicletes per a operaris, estacions i usuaris per a administradors— i aprendrem la regla d'or de l'ordre de les regles amb un exemple que falla; substituirem l'usuari user per usuaris en memòria amb InMemoryUserDetailsManager; estudiarem a fons la codificació de contrasenyes amb DelegatingPasswordEncoder i BCrypt; i decidirem, amb criteri i no per costum, què fer amb el CSRF, la sessió i les capçaleres de seguretat. La xarxa de Ribalta tindrà per fi els seus primers panys.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats