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
- Per què la seguretat no s'improvisa
- Autenticació, autorització i auditoria
- Què passa en afegir
spring-boot-starter-security - La cadena de filtres de servlet
- Els filtres importants, en ordre
- El model d'objectes de Spring Security
- El flux d'autenticació genèric
SecurityContextHolderi elThreadLocal- Mecanismes d'autenticació disponibles
- L'OWASP Top 10 aplicat a una API REST
- Errors Comuns i Consells
- Exercicis
- 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 totSHA-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
equalsacaba 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=xi/api/v1/%65stacionspoden arribar al mateix controlador i esquivar una comprovació ingènua basada enstartsWith.
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.
- 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.
- Què passa en afegir
spring-boot-starter-security
spring-boot-starter-securityLa 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:
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 RibaltaL'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 taulausuarisde 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
Authorizationviatja 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).
- 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.
- 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:
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.
- 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.
- 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:
- El filtre no valida res. Només extreu credencials del transport (capçalera, formulari o JSON) i construeix un
Authenticationsense autenticar. Per això hi ha un filtre diferent per mecanisme i tots acaben al mateixAuthenticationManager. ProviderManagerés la implementació habitual d'AuthenticationManageri 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ó.- La comparació de contrasenyes passa a l'
AuthenticationProvider, amb elPasswordEncoder, no alUserDetailsService. - L'
Authenticationretornat é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. - Desar el resultat al
SecurityContextHolderés responsabilitat del filtre, no del gestor. És exactament el que farà el nostreFiltreAutenticacioJwta 05-04.
SecurityContextHolder i el ThreadLocal
SecurityContextHolder i el ThreadLocalEl 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.
- 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
Bearerl'entén qualsevol client: l'app mòbil, el tauler web,curli 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.
- 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:
- Què retorna
GET /api/v1/estacionssense credencials? I amb les credencials del log? - Què retorna
POST /api/v1/estacionsamb les credencials correctes i un cos vàlid? Explica el resultat. - Continua accessible
/swagger-ui.html? - Què passa si defineixes
spring.security.user.nameispring.security.user.passwordaapplication.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
DelegatingFilterProxyrep la petició de Tomcat i delega en el beanspringSecurityFilterChain.FilterChainProxybusca la primeraSecurityFilterChainque coincideix. Només hi ha la de per defecte, que s'aplica a/**.SecurityContextHolderFilterintenta carregar unSecurityContextde la sessió. No hi ha galetaJSESSIONID, així que el context queda buit.HeaderWriterFilterprepara les capçaleres de seguretat de la resposta.CsrfFilters'activa perquèPOSTsí que modifica estat. Busca el token anti-CSRF al paràmetre_csrfo a la capçaleraX-CSRF-TOKEN. No el troba i llançaInvalidCsrfTokenException(oMissingCsrfTokenException).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 propiAccessDeniedHandler.- La resposta és
403 Forbidden.BasicAuthenticationFilter,AuthorizationFilter, elDispatcherServlet,EstacioControlleriEstacioServiceno 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
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
