CicloUrbana ja sap qui és cada ciutadà de Ribalta, però ho pregunta d'una manera que no aguanta l'ús real. Amb HTTP Basic, l'aplicació mòbil ha de desar la contrasenya de la Marta i enviar-la a cada petició: en obrir el mapa, en consultar una estació, en iniciar un lloguer. La credencial més valuosa de l'usuari recorre la xarxa desenes de vegades al dia, el servidor calcula un BCrypt de noranta mil·lisegons a cadascuna, i no hi ha manera de fer caducar un accés sense obligar a canviar la contrasenya.
Aquesta lliçó ho resol amb el patró estàndard de les API modernes: la contrasenya s'envia una sola vegada, i a canvi el servidor emet un token signat i caducable que el client presenta després a la capçalera Authorization: Bearer. Veurem què és exactament un JWT camp a camp, l'implementarem amb JJWT i un filtre propi, hi afegirem tokens de refresc amb rotació i revocació en base de dades, i afrontarem amb honestedat les dues preguntes incòmodes del model sense estat: on desa el client el token i què significa realment tancar la sessió.
Advertiment. Tots els secrets, claus i tokens d'aquesta lliçó són ficticis i estan truncats; cap no s'ha de reutilitzar. El secret de signatura no s'escriu mai al repositori: s'injecta per variable d'entorn o gestor de secrets. I una implementació pròpia de JWT, encara que faci servir una llibreria sòlida, ha de ser revisada per un professional de seguretat abans d'exposar-se a Internet; l'apartat 15 explica l'alternativa que evita escriure aquest codi.
Contingut
- Per què una app mòbil necessita autenticació sense estat
- Sessió amb galeta davant de token: la comparació honesta
- Anatomia d'un JWT
- Els claims: estàndard i propis de CicloUrbana
- Algorismes de signatura: HS256 davant de RS256
- Signat no vol dir xifrat
- JJWT i
JwtProperties ServeiJwt: generar, llegir i validarFiltreAutenticacioJwt- Registrar el filtre a la cadena de seguretat
POST /api/v1/auth/login- Tokens de refresc: rotació i revocació
- Tancar la sessió en un món sense estat
- On desa el token el client
- Documentar l'esquema a OpenAPI i l'alternativa per a producció
- Provar el flux complet amb curl
- Errors Comuns i Consells
- Exercicis
- Per què una app mòbil necessita autenticació sense estat
L'alternativa clàssica al token és la sessió al servidor: l'usuari entra una vegada, el servidor desa el seu estat en memòria i li lliura una galeta amb un identificador. Funciona molt bé per a un web tradicional i molt malament per a CicloUrbana:
- El client no és un navegador. Una aplicació mòbil no gestiona galetes de manera natural, i la política de tercers dels navegadors complica cada vegada més l'escenari web.
- La sessió viu en una instància. Amb tres rèpliques darrere d'un balancejador, la sessió de la Marta és a la número 2 i les altres dues no la coneixen: les sortides són sessió apegalosa (fràgil) o replicació (cara). Amb un token, qualsevol instància atén qualsevol petició.
- Contradiu la restricció REST de 03-01, que exigeix que cada petició contingui tota la informació necessària, i consumeix memòria proporcional al nombre d'usuaris connectats.
La solució sense estat inverteix l'emmagatzematge: l'estat viatja amb el client, signat pel servidor perquè no el pugui manipular. El servidor no recorda res; només verifica una signatura.
- Sessió amb galeta davant de token: la comparació honesta
| Sessió amb galeta | Token JWT | |
|---|---|---|
| On viu l'estat | Al servidor | Al client |
| Escala horitzontal | Requereix sessió apegalosa o magatzem compartit | Trivial |
| Revocació immediata | Sí: s'esborra la sessió | No: val fins que caduca |
| Mida per petició | ~50 bytes | 300–1000 bytes |
| Canvi de rols / client mòbil | Immediat / incòmode | Fins a la caducitat / natural |
| CSRF | Vulnerable, exigeix token anti-CSRF | No s'aplica si va a la capçalera |
| XSS | La galeta HttpOnly no és llegible per JS |
Greu si es desa a localStorage |
Els tres inconvenients del JWT s'han de dir sense adorns, perquè gairebé mai no s'esmenten:
No es pot revocar fàcilment. Un token robat és vàlid fins que expira i no hi ha res al servidor que l'invalidi: si la Marta perd el mòbil, el seu token continua funcionant. La defensa principal és la caducitat curta, i per això l'apartat 12 introdueix els tokens de refresc. Ocupa espai: un JWT amb rols ronda els 400 bytes que viatgen a cada petició, un cost real sobre una connexió mòbil. I és perillós si s'emmagatzema malament: desar-lo a localStorage el fa llegible per qualsevol JavaScript de la pàgina, de manera que un XSS passa de ser un problema molest a un robatori d'identitat complet (apartat 14).
Un JWT no és la millor opció sempre. Per a una aplicació web clàssica amb vistes al servidor, la sessió amb galeta continua sent més simple i més segura. CicloUrbana tria el token perquè el seu client és mòbil i la seva API és sense estat, no perquè sigui modern.
- Anatomia d'un JWT
Un JSON Web Token (RFC 7519) són tres blocs codificats en Base64URL i separats per punts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYXJ0YUByaWJhbHRhLmVzIiwia WRVc3VhcmlvIjo0MiwiaXNzIjoiY2ljbG91cmJhbmEifQ.K3Bn1sV7oQ2xJ0dLpR8mFq4tYc9Z └──────── capçalera ───────┘ └──────────── càrrega útil ───────────┘ └ signatura ┘
En descodificar els dos primers blocs —que és trivial: echo '<bloc>' | base64 -d— s'obté JSON:
{ "iss": "ciclourbana",
"sub": "[email protected]",
"idUsuari": 42,
"rols": ["ROLE_CIUTADA"],
"iat": 1773561600,
"exp": 1773562500,
"jti": "8f2a4c31-5b7e-4d19-9c02-6ea3f1b0d47c" }I el tercer bloc, la signatura, es calcula així:
Base64URL no és xifratge: és una codificació reversible que existeix només perquè el token viatgi sense caràcters problemàtics en un URL o una capçalera. Qualsevol pot llegir la càrrega útil; el que ningú no pot fer sense el secret és produir una signatura vàlida. La verificació funciona així:
flowchart LR
S["Separar<br/>capçalera . càrrega . signatura"] --> R["Recalcular l'HMAC<br/>amb el secret del servidor"]
R --> C{"Coincideix<br/>la signatura?"}
C -- "No" --> X["Manipulat o fals → 401"]
C -- "Sí" --> E{"exp és al<br/>futur?"}
E -- "No" --> Y["Caducat → 401"]
E -- "Sí" --> OK["Autenticar"]
Si un atacant canvia "rols":["ROLE_CIUTADA"] per "rols":["ROLE_ADMIN"], la càrrega útil canvia, l'HMAC recalculat ja no coincideix amb la signatura que acompanya el token i el servidor el rebutja. No pot recalcular la signatura correcta perquè no té el secret.
- Els claims: estàndard i propis de CicloUrbana
Cada camp de la càrrega útil és un claim, una afirmació sobre el subjecte. L'RFC en defineix set de registrats:
| Claim | Nom | Per a què serveix | A CicloUrbana |
|---|---|---|---|
iss |
Issuer | Qui va emetre el token | ciclourbana |
sub |
Subject | De qui parla | El correu de l'usuari |
aud |
Audience | Per a qui és vàlid | ciclourbana-api |
exp |
Expiration | Instant a partir del qual no val | Emissió + 15 minuts |
nbf / iat |
Not before / Issued at | Des de quan val, quan es va emetre | Només iat |
jti |
JWT ID | Identificador únic del token | UUID, per a revocació |
Els tres primers importen més del que sembla. Validar iss i aud evita que un token emès per un altre sistema —o per a un altre servei de la mateixa organització— sigui acceptat aquí, una fallada real i freqüent en arquitectures amb diversos serveis; i jti és el que fa possible una llista de revocació (apartat 13). Hi afegeix, CicloUrbana, dos claims propis:
| Claim propi | Tipus | Per què |
|---|---|---|
rols |
Llista de cadenes | Evita consultar la base de dades a cada petició per saber què pot fer l'usuari |
idUsuari |
Número | El mateix motiu pel qual UsuariAutenticat desa l'id (05-03): permet comprovar la propietat d'un lloguer sense consultar |
Cada claim que hi afegeixes és un compromís. Fa el token més gran i, sobretot, congela una dada: si un administrador retira el rol OPERARI al Lluís, el seu token continua dient que el té fins que caduqui. Amb quinze minuts de vida és assumible; amb vint-i-quatre hores, no. És la limitació que vam anticipar a l'exercici 3 de 05-03, i la raó de fons que el token d'accés hagi de ser curt.
- Algorismes de signatura: HS256 davant de RS256
| HS256 (HMAC-SHA256) | RS256 (RSA-SHA256) | |
|---|---|---|
| Tipus | Simètric: un secret compartit | Asimètric: parell de claus |
| Qui signa / verifica | El mateix secret per a totes dues coses | La privada signa, la pública verifica |
| Mida de la signatura i velocitat | 32 bytes, molt ràpid | 256 bytes, signatura lenta |
| Risc principal | El secret és a tots els qui verifiquen | Gestió del parell de claus |
| Quan fer-lo servir | Un sol servei emet i verifica | Diversos serveis verifiquen; proveïdor extern |
CicloUrbana fa servir HS256, perquè avui la mateixa aplicació emet i verifica els tokens i un secret compartit és la solució més simple i perfectament segura en aquest escenari. Cal canviar a RS256 tan bon punt més d'un servei hagi de verificar: amb HS256, qui pot verificar també pot emetre, així que compartir el secret amb cinc microserveis significa que cinc equips poden fabricar tokens d'administrador. Amb RS256 els serveis reben només la clau pública i verifiquen sense poder falsificar. És l'escenari del mòdul 7, i el motiu que els proveïdors d'identitat facin servir sempre algorismes asimètrics.
La vulnerabilitat
alg: none. Les primeres implementacions de JWT acceptaven un token la capçalera del qual declarés"alg":"none"i el donaven per vàlid sense signatura. Un atacant només havia de treure la signatura i canviar la capçalera. Una altra variant consistia a canviarRS256perHS256perquè el servidor fes servir la clau pública com a secret HMAC —una clau que l'atacant coneix—. La defensa és la mateixa en tots dos casos: l'algorisme esperat el decideix el servidor, mai el token. JJWT 0.12 ho fa correctament si es fa servirverifyWith(clau), que fixa l'algorisme a partir del tipus de clau. No escriguis mai codi que llegeixialgde la capçalera per decidir com verificar.
- Signat no vol dir xifrat
Advertiment destacat. La càrrega útil d'un JWT és llegible per qualsevol que tingui el token: n'hi ha prou de descodificar Base64URL, sense secrets ni eines. La signatura garanteix integritat i autenticitat, no confidencialitat.
El que mai no ha d'anar en un JWT:
- Contrasenyes o resums de contrasenyes.
- DNI, adreça postal, telèfon, dades de salut o qualsevol dada personal sensible.
- Números de targeta, dades bancàries, claus d'API, secrets interns o rutes d'infraestructura.
- Informació comercial reservada, com l'import acumulat dels lloguers.
El que sí que és raonable: l'identificador de l'usuari, el seu correu si el servei el tracta com a identificador públic, els seus rols i les marques de temps. La regla pràctica: si no ho escriuries en una postal, no ho posis en un JWT. Quan de debò cal confidencialitat existeix JWE (JSON Web Encryption), que xifra la càrrega útil; és notablement més complex, i la solució habitual és més simple: no ficar la dada al token i consultar-la al servidor quan calgui.
- JJWT i
JwtProperties
JwtProperties<!-- Les tres amb <version>0.12.6</version>: JJWT no el gestiona l'starter-parent -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId>
<version>0.12.6</version></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId>
<version>0.12.6</version><scope>runtime</scope></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version><scope>runtime</scope></dependency>Les tres són necessàries i el repartiment és deliberat: jjwt-api conté les interfícies i és l'única amb què compiles; jjwt-impl i jjwt-jackson són implementacions en àmbit runtime, cosa que impedeix acoblar el teu codi a detalls interns. Si apareix un ClassNotFoundException sobre DefaultJwtBuilder, és que falta jjwt-impl. I compte: JJWT no està gestionat pel spring-boot-starter-parent, així que la versió cal fixar-la a mà i revisar-la periòdicament (05-05).
La configuració, amb @ConfigurationProperties tipades com a 02-05:
# application.yml
ciclourbana:
jwt:
emissor: ciclourbana
audiencia: ciclourbana-api
expiracio: 15m # token d'accés: curt a propòsit
expiracio-refresc: 30d
secret: ${JWT_SECRET} # MAI un valor literal aquípackage com.ciclourbana.seguretat;
@ConfigurationProperties(prefix = "ciclourbana.jwt")
@Validated
public record JwtProperties(
@NotBlank @Size(min = 43) String secret, // 43 caràcters Base64 = 256 bits
@NotBlank String emissor,
@NotBlank String audiencia,
@NotNull Duration expiracio,
@NotNull Duration expiracioRefresc) {
public JwtProperties {
if (expiracio.compareTo(Duration.ofHours(1)) > 0) {
throw new IllegalArgumentException(
"El token d'accés no ha de durar més d'una hora; fes servir refresc");
}
}
}Quatre decisions importants:
El secret ve d'una variable d'entorn. ${JWT_SECRET} sense valor per defecte: si la variable no existeix, l'aplicació no arrenca, que és exactament el que es vol. Escriure un secret per defecte al YAML és pitjor que no tenir seguretat, perquè crea una falsa sensació de tenir-ne: acaba a Git, a la imatge Docker i al portàtil de tot l'equip, i aquest mateix valor acaba en producció amb una freqüència que fa por.
@Size(min = 43) no és arbitrari. HS256 exigeix una clau de com a mínim 256 bits, que en Base64 són 43 caràcters; JJWT rebutja claus més curtes amb WeakKeyException, i validar-ho en arrencar converteix aquella fallada en temps d'execució en un error de configuració comprensible. Un secret adequat es genera, mai no s'inventa:
openssl rand -base64 48 # 64 caràcters Base64 = 384 bits
export JWT_SECRET='...' # el valor real, mai en un fitxer del repositoriEl constructor compacte valida la política, no només el format: que algú configuri expiracio: 24h no és un error de tipus però sí de seguretat, i l'arrencada ho impedeix. I el secret es rota, com qualsevol credencial: en producció viu en un gestor de secrets (Vault, AWS Secrets Manager, els secrets de Kubernetes) i es canvia periòdicament, sabent que rotar-lo invalida tots els tokens en circulació.
ServeiJwt: generar, llegir i validar
ServeiJwt: generar, llegir i validarpackage com.ciclourbana.seguretat;
@Service
public class ServeiJwt {
private static final Logger log = LoggerFactory.getLogger(ServeiJwt.class);
private final JwtProperties propietats;
private final SecretKey clau;
private final Clock rellotge; // el bean Clock de 03-03
public ServeiJwt(JwtProperties propietats, Clock rellotge) {
this.propietats = propietats;
this.rellotge = rellotge;
// Descodifica el secret Base64 i verifica que tingui prou força
this.clau = Keys.hmacShaKeyFor(Decoders.BASE64.decode(propietats.secret()));
}
/** Emet un token d'accés per a un usuari ja autenticat. */
public String generarToken(UsuariAutenticat usuari) {
Instant ara = Instant.now(rellotge);
return Jwts.builder()
.issuer(propietats.emissor())
.audience().add(propietats.audiencia()).and()
.subject(usuari.getUsername())
.id(UUID.randomUUID().toString()).issuedAt(Date.from(ara)) // jti, iat
.expiration(Date.from(ara.plus(propietats.expiracio())))
.claim("idUsuari", usuari.getIdUsuari())
.claim("rols", usuari.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.signWith(clau, Jwts.SIG.HS256)
.compact();
}
/** Verifica signatura, emissor, audiència i expiració. Llança si alguna cosa falla. */
public Claims extreureClaims(String token) {
return Jwts.parser()
.verifyWith(clau) // fixa l'algorisme: no el llegeix del token
.requireIssuer(propietats.emissor())
.requireAudience(propietats.audiencia())
.clockSkewSeconds(30) // tolerància de rellotge entre màquines
.build().parseSignedClaims(token).getPayload();
}
/** Versió que no llança: útil al filtre, on un token invàlid és rutina. */
public Optional<Claims> validar(String token) {
try {
return Optional.of(extreureClaims(token));
} catch (ExpiredJwtException e) {
log.debug("Token caducat"); // freqüent: NO és un error
} catch (SecurityException | MalformedJwtException e) {
log.warn("Token amb signatura invàlida o mal format"); // això sí que és sospitós
} catch (JwtException | IllegalArgumentException e) {
log.warn("Token no vàlid: {}", e.getClass().getSimpleName());
}
return Optional.empty(); // mai no es registra el token al log
}
}Cinc detalls que fan que aquest codi sigui correcte i no només funcional:
verifyWith(clau)fixa l'algorisme a partir del tipus de clau. És el que tanca la porta aalg: nonei a la confusió RS256/HS256 de l'apartat 5.requireIssuerirequireAudiencerebutgen tokens aliens. S'obliden gairebé sempre.clockSkewSeconds(30)tolera el desfasament de rellotge entre màquines. Sense això, dos servidors amb dos segons de diferència produeixen rebutjos intermitents impossibles de reproduir. I elClockinjectat —el bean de 03-03— farà possible provar la caducitat al mòdul 6 sense esperar quinze minuts.- Un token caducat es registra en
DEBUG; una signatura invàlida, enWARN. El primer és el funcionament normal; el segon pot ser un atac, i barrejar-los fa inútil el log (09-05). - Mai no es registra el token: un token en un log és una credencial en un log. I
.audience().add(...).and()és l'API fluida de JJWT 0.12, que va canviar respecte a la 0.11, així que molt codi d'internet no compilarà contra aquesta versió.
FiltreAutenticacioJwt
FiltreAutenticacioJwtEl filtre és la peça que converteix una capçalera HTTP en un usuari autenticat. Hereta d'OncePerRequestFilter, igual que el FiltreRastreig de 03-06, per garantir una sola execució per petició encara que hi hagi reenviaments interns.
package com.ciclourbana.seguretat;
@Component
public class FiltreAutenticacioJwt extends OncePerRequestFilter {
private static final String CAPCALERA = "Authorization";
private static final String PREFIX = "Bearer ";
private final ServeiJwt serveiJwt; // constructor omès
@Override
protected void doFilterInternal(HttpServletRequest peticio,
HttpServletResponse resposta,
FilterChain cadena) throws ServletException, IOException {
extreureToken(peticio).flatMap(serveiJwt::validar)
.ifPresent(claims -> autenticar(claims, peticio));
// SEMPRE es continua: rebutjar és feina de l'AuthorizationFilter
cadena.doFilter(peticio, resposta);
}
private Optional<String> extreureToken(HttpServletRequest peticio) {
String capcalera = peticio.getHeader(CAPCALERA);
return (capcalera != null && capcalera.startsWith(PREFIX))
? Optional.of(capcalera.substring(PREFIX.length()).trim()) : Optional.empty();
}
@SuppressWarnings("unchecked")
private void autenticar(Claims claims, HttpServletRequest peticio) {
if (SecurityContextHolder.getContext().getAuthentication() != null) return;
List<String> rols = claims.get("rols", List.class);
var autoritats = rols.stream().map(SimpleGrantedAuthority::new).toList();
var usuari = new UsuariAutenticat(claims.get("idUsuari", Integer.class).longValue(),
claims.getSubject(), autoritats);
var autenticacio = new UsernamePasswordAuthenticationToken(
usuari, null, autoritats); // credencials: null, ja està autenticat
autenticacio.setDetails(new WebAuthenticationDetailsSource().buildDetails(peticio));
SecurityContextHolder.getContext().setAuthentication(autenticacio);
}
}Cinc decisions que convé entendre:
El filtre no rebutja mai la petició. Si no hi ha token, o és invàlid, simplement no autentica i deixa continuar la cadena: qui decideix si aquella petició necessitava autenticació és l'AuthorizationFilter de 05-01, i qui produeix el 401 és l'AuthenticationEntryPoint de 05-03. Aquesta separació és la que permet que GET /api/v1/estacions continuï sent públic mentre /api/v1/lloguers no ho és, amb el mateix filtre.
No es consulta la base de dades: tota la informació —id, correu, rols— ve del token. És el que fa el model sense estat i ràpid, i també el que congela els rols fins a la caducitat; l'alternativa, carregar el UserDetails a cada petició, és més segura i sacrifica la major part de l'avantatge.
Es comprova si ja hi ha autenticació abans d'escriure al context, per no trepitjar la d'un altre mecanisme. UsuariAutenticat necessita un segon constructor que rebi id, correu i autoritats, sense entitat ni resum: getPassword() retornarà null i no passa res, perquè en aquest camí ningú no compara contrasenyes. I es desen els details amb la IP: informació valuosa per a l'auditoria de 05-03.
- Registrar el filtre a la cadena de seguretat
@Bean
SecurityFilterChain cadenaApi(HttpSecurity http,
FiltreAutenticacioJwt filtreJwt,
PuntEntradaNoAutenticat puntEntrada,
GestorAccesDenegat accesDenegat) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // justificat a 05-02
.cors(Customizer.withDefaults())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(AbstractHttpConfigurer::disable) // adeu a la contrasenya a cada petició
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable) // no hi ha sessió per tancar
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/v1/auth/**").permitAll()
.anyRequest().denyAll()) // ... i la resta del mapa de 05-02
.exceptionHandling(ex -> ex.authenticationEntryPoint(puntEntrada)
.accessDeniedHandler(accesDenegat))
.addFilterBefore(filtreJwt, UsernamePasswordAuthenticationFilter.class);
return http.build();
}addFilterBefore(filtreJwt, UsernamePasswordAuthenticationFilter.class) col·loca el filtre en una posició concreta de la cadena de 05-01: després de SecurityContextHolderFilter i CorsFilter —així el context és net i les peticions de sondeig CORS ja s'han resolt— i abans d'ExceptionTranslationFilter i AuthorizationFilter, que és l'imprescindible: quan l'autorització avaluï les regles, el context ja ha de contenir l'usuari. I aquí desapareixen httpBasic i formLogin, tal com vam anunciar a 05-02: a partir d'ara l'única manera d'autenticar-se contra l'API és un token.
POST /api/v1/auth/login
POST /api/v1/auth/loginpublic record LoginRequest(@NotBlank @Email String correu, @NotBlank String contrasenya) {}
public record TokenResponse(String tokenAcces, String tokenRefresc,
String tipus, long expiraEnSegons) {}
@Service
public class ServeiAutenticacio {
private final AuthenticationManager gestorAutenticacio; // bean de 05-03
private final ServeiJwt serveiJwt;
private final ServeiTokenRefresc serveiRefresc;
private final JwtProperties propietats; // constructor omès
public TokenResponse autenticar(LoginRequest peticio) {
// Delega TOTA la validació en el DaoAuthenticationProvider de 05-03:
// resum, compte actiu, bloqueig i protecció contra temporització
Authentication autenticacio = gestorAutenticacio.authenticate(
new UsernamePasswordAuthenticationToken(
peticio.correu().trim().toLowerCase(Locale.ROOT),
peticio.contrasenya()));
var usuari = (UsuariAutenticat) autenticacio.getPrincipal();
return new TokenResponse(
serveiJwt.generarToken(usuari),
serveiRefresc.emetre(usuari.getIdUsuari()).token(),
"Bearer",
propietats.expiracio().toSeconds());
}
}El controlador és un @PostMapping("/login") a l'AutenticacioController de 05-03 que rep @Valid @RequestBody LoginRequest i retorna 200 OK amb el TokenResponse.
Tres punts. L'autenticació no es reimplementa: es delega en l'AuthenticationManager, que ja sap comparar resums en temps constant, comprovar si el compte està actiu i publicar els esdeveniments de fallada; reescriure aquí un if (codificador.matches(...)) perdria tot això. Si falla llança BadCredentialsException, que cal traduir a un 401 uniforme a GestorGlobalExcepcions —aquest sí que la veu, perquè passa dins d'un controlador—. I la resposta no revela res de l'usuari: només tokens.
- Tokens de refresc: rotació i revocació
Hi ha una tensió evident. Un token d'accés curt limita el dany d'un robatori, però obliga el ciutadà a escriure la contrasenya cada quinze minuts. Un token llarg és còmode i perillós.
La solució són dos tokens amb papers diferents:
| Token d'accés | Token de refresc | |
|---|---|---|
| Format i durada | JWT signat, 15 minuts | Cadena aleatòria opaca, 30 dies |
| On es fa servir | A cada petició a l'API | Només a /api/v1/auth/refresc |
| El servidor el desa? Es revoca? | No i no | Sí, en base de dades; revocable a l'instant |
El token de refresc no és un JWT, i és deliberat: com que el servidor el desa igualment per poder-lo revocar, no hi ha cap avantatge que sigui autocontingut, i una cadena aleatòria llarga és més curta, més opaca i no revela res.
-- V6__crear_tokens_refresc.sql
-- (V5 va normalitzar els correus, a l'exercici 2 de 05-03)
CREATE SEQUENCE tokens_refresc_id_seq INCREMENT BY 50 START WITH 1;
CREATE TABLE tokens_refresc (
id BIGINT NOT NULL,
usuari_id BIGINT NOT NULL,
token_hash VARCHAR(64) NOT NULL, -- SHA-256 del token, mai el token
expira_el TIMESTAMPTZ NOT NULL,
revocat_el TIMESTAMPTZ,
creat_el TIMESTAMPTZ NOT NULL,
ip_origen VARCHAR(45),
CONSTRAINT pk_tokens_refresc PRIMARY KEY (id),
CONSTRAINT uk_tokens_refresc_hash UNIQUE (token_hash),
CONSTRAINT fk_tokens_refresc_usuari FOREIGN KEY (usuari_id)
REFERENCES usuaris (id) ON DELETE CASCADE
);
CREATE INDEX ix_tokens_refresc_usuari ON tokens_refresc (usuari_id);
CREATE INDEX ix_tokens_refresc_expira ON tokens_refresc (expira_el);Es desa el SHA-256 del token, no el token, pel mateix raonament que amb les contrasenyes: si algú llegeix la taula, no obté credencials utilitzables. Aquí n'hi ha prou amb un resum ràpid, perquè el token és una cadena aleatòria de 256 bits i no una contrasenya endevinable per diccionari. L'endpoint de refresc, amb rotació:
@Transactional
public TokenResponse refrescar(String tokenPresentat) {
String hash = sha256(tokenPresentat);
TokenRefresc desat = repositori.findByTokenHash(hash)
.orElseThrow(() -> new BadCredentialsException("Token de refresc no vàlid"));
if (desat.getRevocatEl() != null) { // reutilització: senyal de robatori, es talla tot
log.error("REUTILITZACIÓ de token de refresc. Usuari {}", desat.getUsuariId());
repositori.revocarTotsDelUsuari(desat.getUsuariId(), Instant.now(rellotge));
throw new BadCredentialsException("Token de refresc no vàlid");
}
if (desat.getExpiraEl().isBefore(Instant.now(rellotge)))
throw new BadCredentialsException("Token de refresc no vàlid");
desat.setRevocatEl(Instant.now(rellotge)); // rotació: el vell mor
var usuari = carregarUsuari(desat.getUsuariId()); // rols frescos de la BD
return new TokenResponse(serveiJwt.generarToken(usuari),
emetre(usuari.getIdUsuari()).token(),
"Bearer", propietats.expiracio().toSeconds());
}Quatre idees clau:
Rotació: cada ús del token de refresc l'invalida i n'emet un de nou. Redueix la finestra d'un token robat i, sobretot, fa detectable el robatori.
Detecció de reutilització: si arriba un token ja revocat, o bé és el lladre fent servir un que la víctima ja va rotar, o bé a l'inrevés; no hi ha manera de saber quin, així que es revoquen tots els tokens de refresc d'aquell usuari i se l'obliga a autenticar-se de nou. És molest i és el correcte.
Els rols es recarreguen de la base de dades, i és el moment en què el canvi de privilegis de l'exercici 3 de 05-03 fa efecte: com a màxim quinze minuts després. El missatge d'error és sempre el mateix, tant si el token no existeix, com si està revocat o ha caducat. I falta una peça operativa: una tasca programada que esborri els caducats, o la taula creix sense límit (07-03).
- Tancar la sessió en un món sense estat
POST /api/v1/auth/logout no pot invalidar el token d'accés. Està signat, és vàlid i el servidor no desa res sobre ell. Les opcions reals:
| Estratègia | Efecte | Cost |
|---|---|---|
| Esborrar el token al client | El client deixa d'enviar-lo | Nul. No protegeix si ja va ser robat |
| Revocar el token de refresc | En ≤15 min l'accés mor | Baix. L'opció de CicloUrbana |
Llista de revocació per jti |
Immediat | Un magatzem consultat a cada petició: deixa de ser sense estat |
CicloUrbana implementa el tancament de sessió com a revocació del token de refresc: el token d'accés continua valent fins a quinze minuts i després no es pot renovar. És una decisió conscient, i cal ser honestos sobre el que significa: durant aquests quinze minuts, un token robat continua funcionant.
Quan això no és tolerable —una operació bancària, un tancament de sessió per sospita de compromís— la solució és una llista de revocació dels jti en una memòria cau ràpida com Redis, amb expiració automàtica en caducar el token; consultar-la a cada petició reintrodueix estat, però acotat, i la memòria cau distribuïda es tracta a 09-02. En qualsevol cas, la defensa principal continua sent l'expiració curta: tota la resta són mitigacions.
- On desa el token el client
És la part del sistema que el backend no controla i on es produeixen més incidents reals.
| Emmagatzematge | Vulnerable a XSS | Vulnerable a CSRF | Notes |
|---|---|---|---|
localStorage / sessionStorage |
Sí, totalment | No | Llegible per qualsevol JS de la pàgina |
Galeta HttpOnly + Secure + SameSite=Strict |
No | Sí, requereix CSRF | El JS no la pot llegir |
| Memòria del JS (una variable) | Parcialment | No | Es perd en recarregar; el refresc en galeta ho mitiga |
| Clauer del sistema (mòbil) | No s'aplica | No s'aplica | La millor opció en una app nativa |
Advertiment.
localStorageés el consell més repetit d'internet i el pitjor. Qualsevol XSS —una dependència de JavaScript compromesa, un camp mal escapat— permet llegir el token i suplantar l'usuari des d'una altra màquina. Una galetaHttpOnlyno és llegible per JavaScript ni tan sols amb XSS.
L'app mòbil de CicloUrbana, que és el client principal, fa servir el clauer del sistema: Keychain a iOS, EncryptedSharedPreferences o Keystore a Android. Per al tauler web de l'ajuntament la recomanació és la galeta HttpOnly; Secure; SameSite=Strict amb protecció CSRF reactivada —recorda l'advertiment de 05-02: en tornar la credencial a una galeta, la condició que permetia desactivar el CSRF deixa de complir-se—, o bé el patró mixt de token d'accés en memòria i refresc en galeta HttpOnly. I en qualsevol cas, HTTPS obligatori: sense TLS tot l'anterior és irrellevant, perquè el token viatja llegible per la xarxa (05-05).
- Documentar l'esquema a OpenAPI i l'alternativa per a producció
El Swagger UI de 03-07 va deixar de funcionar contra els endpoints protegits: no té on escriure el token. S'arregla declarant l'esquema de seguretat a ConfiguracioOpenApi:
@Bean
OpenAPI apiCicloUrbana(...) {
return new OpenAPI()
.info(...) // igual que a 03-07
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components().addSecuritySchemes("bearerAuth",
new SecurityScheme().type(SecurityScheme.Type.HTTP)
.scheme("bearer").bearerFormat("JWT")
.description("Token obtingut a POST /api/v1/auth/login")));
}Apareix llavors el botó Authorize a la interfície i springdoc afegeix la capçalera a totes les crides; els endpoints públics es marquen amb @SecurityRequirements buit perquè la documentació no menteixi.
L'alternativa recomanada per a producció
Tot el codi d'aquesta lliçó és una implementació pròpia: excel·lent per entendre el mecanisme, i en un sistema real convé no escriure'l. Spring ofereix un mòdul mantingut i auditat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>spring:
security:
oauth2:
resourceserver:
jwt:
# El proveïdor publica les seves claus públiques; Spring les baixa i les rota sol
issuer-uri: https://identitat.ribalta.example/realms/ciclourbanaAmb aquestes línies desapareixen ServeiJwt, FiltreAutenticacioJwt i la gestió de secrets: el BearerTokenAuthenticationFilter de 05-01 se'n fa càrrec fent servir Nimbus per sota, i les claus públiques s'obtenen del JWKS del proveïdor i es roten soles.
| Implementació pròpia (aquesta lliçó) | Resource Server + proveïdor | |
|---|---|---|
| Codi que mantens | ~200 línies de seguretat | Pràcticament cap |
| Emissió de tokens i rotació de claus | Teva, manual | Del proveïdor (Keycloak, Auth0, Cognito), automàtica via JWKS |
| Segon factor, OIDC, SSO | Caldria implementar-ho | Inclòs |
| Auditoria del codi | Responsabilitat teva | Del proveïdor |
| Infraestructura extra | Cap | Un servei més |
El criteri: si el sistema té un sol servei, un client i requisits senzills, la implementació pròpia amb una llibreria sòlida és acceptable. Tan bon punt apareguin diversos serveis, inici de sessió amb proveïdors externs, segon factor o requisits de compliment normatiu, fes servir un proveïdor d'identitat. El cost de mantenir seguretat pròpia creix molt més ràpid del que sembla.
- Provar el flux complet amb curl
# 1. Inici de sessió (després del registre de 05-03): la contrasenya viatja UNA sola vegada
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"correu":"[email protected]","contrasenya":"clau-llarga-ex"}' | jq -r .tokenAcces)
# 3 i 4. Sense token i amb token
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/api/v1/lloguers # 401
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/lloguers # 200
# 5. Inspeccionar la càrrega útil (sense secret: demostra que NO va xifrada)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
# {"iss":"ciclourbana","sub":"[email protected]","idUsuari":42,"rols":[...],...}
# 6. La Marta és CIUTADA: la gestió d'estacions li està vedada
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"nom":"Mercat Vell","capacitat":20}' \
http://localhost:8080/api/v1/estacions # 403
# 7. Token manipulat: canviar un sol caràcter trenca la signatura
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer ${TOKEN}x" http://localhost:8080/api/v1/lloguers # 401Els passos 5 i 7 són els importants: el 5 demostra que el token és llegible per qualsevol —d'aquí l'advertiment de l'apartat 6— i el 7, que no és modificable.
Errors Comuns i Consells
Posar dades sensibles a la càrrega útil. Va signada, no xifrada: un DNI en un JWT és un DNI públic.
Escriure el secret a application.yml, o fer-ne servir un de curt i inventat. Un secret al YAML acaba a Git, a la imatge i al portàtil de tot l'equip; i elmeusecret123 no arriba a 256 bits. Variable d'entorn sense valor per defecte, generat amb openssl rand -base64 48. I res de tokens d'accés de 24 hores «perquè no molesti»: multipliquen per 96 la finestra d'un robatori i congelen els rols tot aquest temps.
Que el filtre retorni 401 directament, o registrar-lo en la posició equivocada de la cadena. El primer trenca els endpoints públics —el filtre autentica si pot, i la decisió és de l'AuthorizationFilter—; el segon, si queda després de l'AuthorizationFilter, fa que tot retorni 401.
No validar iss ni aud, amb la qual cosa un token emès per un altre sistema amb el mateix secret seria acceptat; o llegir alg del token per decidir com verificar, que és la vulnerabilitat alg: none. L'algorisme el fixa el servidor.
Desar el token de refresc en clar a la base de dades, o registrar qualsevol token en un log encara que sigui truncat. Un token és una credencial; desa'n el SHA-256 i no l'escriguis mai.
Consell: expiració curta més refresc amb rotació —15 minuts d'accés, 30 dies de refresc rotatori amb detecció de reutilització— és l'equilibri que funciona. I fes servir el Clock injectat a ServeiJwt: al mòdul 6 podràs provar la caducitat avançant el rellotge en lloc d'esperar.
Exercicis
Exercici 1
Un token de CicloUrbana té aquesta càrrega útil. Detecta quatre problemes de seguretat o disseny i proposa la versió corregida.
{ "sub": "[email protected]", "nom": "Marta Aguiló", "dni": "12345678Z",
"contrasenyaHash": "$2a$10$N9qo8uLOickgx2ZMRZoMye...", "idUsuari": 42,
"rols": ["ROLE_CIUTADA"], "importAcumulat": 127.50,
"iat": 1773561600, "exp": 1774166400 }Exercici 2
Implementa ServeiJwt.validar(String) de manera que distingeixi tres resultats —token vàlid, token caducat i token invàlid— mitjançant un tipus propi en lloc d'un Optional, i explica què ha de fer el filtre amb cadascun i a quin nivell de log correspon.
Exercici 3
Dissenya el flux complet per a aquest requisit de l'ajuntament: «quan un ciutadà canvia la seva contrasenya, totes les seves sessions obertes en altres dispositius s'han de tancar immediatament». Tingues en compte que el token d'accés no es pot revocar.
Solucions
Solució 1
Problema 1 — dni. Dada personal identificativa, llegible per qualsevol que tingui el token i subjecta al RGPD. Fora. Problema 2 — contrasenyaHash, la fallada més greu: lliura el resum a qualsevol que capturi el token, i permet un atac de diccionari sense límit d'intents i sense deixar rastre al servidor. Fora, sempre.
Problema 3 — importAcumulat. Dada comercial reservada i, a més, una dada que canvia: quedaria congelada fins a la caducitat i mostraria xifres obsoletes si el client se'n refiés. Es consulta per API. Problema 4 — expiració de set dies (exp - iat = 604 800): un token robat val una setmana, i els canvis de rol triguen una setmana a aplicar-se. Hi falten a més iss, aud i jti, sense els quals no es pot validar la procedència ni revocar per identificador.
{ "iss": "ciclourbana", "aud": "ciclourbana-api", "sub": "[email protected]",
"idUsuari": 42, "rols": ["ROLE_CIUTADA"],
"iat": 1773561600, "exp": 1773562500,
"jti": "8f2a4c31-5b7e-4d19-9c02-6ea3f1b0d47c" }Regla per decidir què hi entra: només allò que es necessita a cada petició per autoritzar, no canvia durant la vida del token i no seria un problema en una postal. El nom s'hi podria quedar si l'app el mostra —no és sensible—, però engreixa el token: és preferible demanar-lo un cop a /api/v1/usuaris/jo.
Solució 2
public sealed interface ResultatValidacio {
record Valid(Claims claims) implements ResultatValidacio {}
record Caducat(String correu) implements ResultatValidacio {}
record Invalid(String motiu) implements ResultatValidacio {}
}
public ResultatValidacio validar(String token) {
try {
return new ResultatValidacio.Valid(extreureClaims(token));
} catch (ExpiredJwtException e) {
// L'excepció de JJWT conserva els claims: útil per al log i per suggerir refresc
return new ResultatValidacio.Caducat(e.getClaims().getSubject());
} catch (SecurityException e) {
return new ResultatValidacio.Invalid("signatura");
} catch (MalformedJwtException e) {
return new ResultatValidacio.Invalid("format");
} catch (JwtException | IllegalArgumentException e) {
return new ResultatValidacio.Invalid(e.getClass().getSimpleName());
}
}
// Al filtre, amb switch de patrons de Java 21:
switch (serveiJwt.validar(token)) {
case ResultatValidacio.Valid v -> autenticar(v.claims(), peticio);
case ResultatValidacio.Caducat c -> { // pista per al client
log.debug("Token caducat de {}", c.correu());
resposta.setHeader("X-Motiu-Auth", "token-caducat");
}
case ResultatValidacio.Invalid i -> log.warn("Token rebutjat ({})", i.motiu());
}Què fa el filtre amb cada cas. Vàlid: autentica. Caducat: no autentica, però tampoc no és un error; és el funcionament normal cada quinze minuts, va a DEBUG, i la capçalera de pista permet a l'app mòbil distingir «renova el token» de «torna a iniciar la sessió» sense exposar res. Invàlid: no autentica i va a WARN, perquè una signatura incorrecta és un intent de manipulació o un client trencat, i un pic d'aquests esdeveniments ha de disparar una alerta (09-05). L'avantatge del tipus segellat sobre l'Optional és que és impossible oblidar un cas: el switch sobre una interfície sealed ha de ser exhaustiu o no compila, i a les proves del mòdul 6 es pot afirmar sobre el motiu exacte.
Solució 3
El problema: el token d'accés dels altres dispositius és vàlid i no hi ha res al servidor que l'invalidi; revocar els tokens de refresc tanca la porta futura, però deixa fins a quinze minuts d'accés vigent. Solució completa, en quatre peces. 1. Marca d'invalidació a l'usuari, amb una migració V7__afegir_invalidacio_tokens.sql que executa ALTER TABLE usuaris ADD COLUMN tokens_invalids_abans_de TIMESTAMPTZ;. 2. En canviar la contrasenya, dins de la mateixa transacció: desar el resum nou, posar tokensInvalidsAbansDe = ara i revocar tots els tokens de refresc de l'usuari. 3. Comprovació al filtre, aprofitant que l'iat del token diu quan es va emetre: si és anterior a la marca, el token mor.
Instant emes = claims.getIssuedAt().toInstant();
Instant tall = serveiUsuaris.invalidacioDe(claims.get("idUsuari", Integer.class));
if (tall != null && emes.isBefore(tall)) return; // no autenticar4. El cost, i com pagar-lo. Això reintrodueix una consulta per petició, just el que el model sense estat evitava. Tres maneres d'esmorteir-ho, de menys a més esforç: cauejar la marca per usuari amb expiració curta (09-02); mantenir a la memòria cau només els usuaris amb invalidació recent, que són poquíssims, i tractar l'absència com a «sense invalidació»; o incloure la marca com a claim al token, encara que llavors només protegeix a partir del refresc següent i no resol el requisit.
La lliçó de fons: el requisit «tancar la sessió immediatament a tot arreu» és incompatible amb l'autenticació purament sense estat. Tota solució paga amb estat al servidor. L'honest és reconèixer-ho, triar on es paga i deixar-ho documentat, no fingir que el JWT ho resol tot. I convé preguntar abans si «immediatament» vol dir de debò zero segons o si quinze minuts són acceptables: la resposta canvia completament l'arquitectura.
Conclusió
CicloUrbana ja autentica com una API moderna. Saps per què una aplicació mòbil necessita autenticació sense estat —el client no és un navegador, la sessió no sobreviu al balancejador i la restricció REST de 03-01 ho exigeix— i has comparat sessió i token amb honestedat, sense amagar els tres inconvenients reals del JWT: no es revoca amb facilitat, ocupa espai a cada petició i és perillós si el client l'emmagatzema malament. Saps també que per a un web clàssic amb vistes al servidor la sessió amb galeta continua sent l'opció més simple i més segura: CicloUrbana tria el token pels seus requisits, no per moda.
Coneixes un JWT per dins: capçalera, càrrega útil i signatura en Base64URL, l'HMAC que lliga les dues primeres parts al secret del servidor, i el fet central que Base64URL no és xifratge. Manegues els claims registrats —iss, sub, aud, exp, iat, jti— i els dos propis del projecte, rols i idUsuari, sabent que cadascun engreixa el token i congela una dada fins a la caducitat. Has comparat HS256 amb RS256 i saps quan obliga a canviar l'arquitectura: tan bon punt un segon servei hagi de verificar, perquè amb un secret compartit qui verifica també pot emetre. I coneixes la vulnerabilitat alg: none i la seva defensa: l'algorisme el decideix el servidor, mai el token.
Has implementat el sistema complet amb JJWT 0.12: les tres dependències amb els seus àmbits, JwtProperties validades amb un secret que arriba per variable d'entorn i sense valor per defecte —de manera que l'aplicació no arrenca si falta—, amb longitud mínima de 43 caràcters Base64 i una comprovació de política que impedeix configurar tokens de més d'una hora. ServeiJwt genera, extreu i valida amb verifyWith, requireIssuer, requireAudience, tolerància de rellotge i un Clock injectat que farà possible provar la caducitat al mòdul 6. FiltreAutenticacioJwt, un OncePerRequestFilter registrat amb addFilterBefore davant d'UsernamePasswordAuthenticationFilter, no rebutja mai: autentica si pot i deixa la decisió a l'AuthorizationFilter, que és just el que permet conviure endpoints públics i protegits a la mateixa cadena. I amb POST /api/v1/auth/login delegant en l'AuthenticationManager de 05-03, l'HTTP Basic i el formulari han desaparegut de la configuració.
Has afegit tokens de refresc amb la taula tokens_refresc de la migració V6, que desa el SHA-256 del token i no el token, amb rotació a cada ús, detecció de reutilització que revoca totes les sessions de l'usuari davant del senyal de robatori i recàrrega de rols des de la base de dades a cada refresc. I has mirat de cara els dos problemes que el màrqueting del JWT sol amagar: tancar la sessió en un món sense estat és revocar el refresc i acceptar fins a quinze minuts d'accés vigent, o pagar amb estat mitjançant una llista de revocació per jti; i l'emmagatzematge al client, on localStorage és el consell més repetit i el pitjor, davant del clauer del sistema a l'app mòbil i la galeta HttpOnly; Secure; SameSite —amb CSRF reactivat— al tauler web. El Swagger UI de 03-07 torna a funcionar amb el seu botó Authorize, i coneixes l'alternativa madura per a producció: spring-boot-starter-oauth2-resource-server amb un proveïdor d'identitat que rota les seves claus per JWKS i porta SSO i segon factor de sèrie.
Queda un forat, i és el mateix que vam deixar anotat a 05-01 i a 05-03. La Marta té un token vàlid amb el rol ROLE_CIUTADA, així que POST /api/v1/lloguers/9/finalitzar passa totes les regles de la cadena: la ruta és legítima i el rol és correcte. Però el lloguer número 9 és d'un altre ciutadà. Cap regla basada en URL no pot expressar «només els teus propis lloguers», perquè la resposta no depèn de la ruta ni del rol, sinó de la dada. És el risc A01/BOLA de l'OWASP Top 10, el més explotat de les API REST. A 05-05, Seguretat a Nivell de Mètode i Enduriment de l'API, baixarem la seguretat fins a la capa de servei amb @EnableMethodSecurity, @PreAuthorize i expressions SpEL, escriurem un avaluador de permisos propi per a la regla dels lloguers, i tancarem el mòdul endurint l'API sencera: CORS per entorn, limitació de taxa, HTTPS obligatori, ocultació de la documentació en producció, auditoria d'esdeveniments de seguretat, revisió de dependències vulnerables i una llista de comprovació abans de producció.
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
