JWT (JSON Web Token) - Resum per Alumnes¶
1. Què és JWT?¶
Un JSON Web Token (JWT) és un estàndard obert de la indústria per transmetre informació entre un client i un servidor de manera segura. Es tracta d'un token codificat en format JSON que s'utilitza principalment per a l'autenticació i autorització en aplicacions web.
A diferència dels sistemes tradicionals que emmagatzemen dades de sessió al servidor, JWT implementa una autenticació sense estat (stateless), on tota la informació necessària es troba dins del propi token.
Avantatges de JWT¶
- Stateless: El servidor no ha de guardar informació de sessió
- Escalable: Funciona perfectament en arquitectures de microserveis
- Segur: Les dades es signen digitalment per verificar la seva autenticitat
- Auto-contingut: El token conté tota la informació necessària sobre l'usuari
- Multiplataforma: Funciona amb aplicacions web, mòbils i APIs
2. Estructura d'un JWT¶
Un JWT consta de tres parts principals, separades per punts (.):
header.payload.signature
2.1 Header (Capçalera)¶
Conté metadades sobre el token:
{
"alg": "HS256",
"typ": "JWT"
}
alg: Algoritme de signatura (HS256, RS256, etc.)typ: Tipus de token (sempre JWT)
2.2 Payload (Càrrega)¶
Conté les dades de l'usuari (claims):
{
"sub": "usuario123",
"username": "joan.garcia",
"email": "joan@example.com",
"iat": 1704067200,
"exp": 1704153600,
"roles": ["user", "admin"]
}
sub: Identificador del subjecte (usuari)username: Nom d'usuariemail: Correu electròniciat(issued at): Moment de creació del token (timestamp)exp(expiration): Moment d'expiració del tokenroles: Rols/permisos de l'usuari
2.3 Signature (Signatura)¶
La signatura es genera cifrando el header i payload amb una clau secreta:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Propòsit: Garantir que el token no ha estat modificat i verificar que la seva procedència és confiable.
Token complet exemple:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3VhcmlvMTIzIiwidXNlcm5hbWUiOiJqb2FuIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
3. Com funciona JWT en l'autenticació?¶
Flux d'autenticació¶
- Login: L'usuari envia les seves credencials (usuari i contrasenya) al servidor
- Validació: El servidor verifica les credencials contra la base de dades
- Generació del Token: Si són correctes, el servidor genera i signa el JWT
- Retorn del Token: El servidor envia el token al client
- Emmagatzematge: El client guarda el token (normalment a
localStorageo sessionStorage) - Peticions posteriors: El client inclou el token a cada petició al servidor
- Verificació: El servidor verifica la signatura del token per autoritzar la petició
Esquema del flux¶
Client Servidor
| |
|--- POST /login (user/pass)---->|
| | Valida credencials
| | Genera JWT
|<--- JWT token ------------------|
| |
| Emmagatzema token |
| |
|--- GET /api/datos + Token ---->|
| | Verifica signatura
| | Si és vàlid, processa
|<--- Dades autoritzades --------|
4. Per què es fa servir JWT?¶
Casos d'ús principals¶
4.1 Single Page Applications (SPAs)¶
Les aplicacions Angular són SPAs que necessiten autenticació stateless per funcionar correctament. JWT és ideal perquè: - L'aplicació frontend pot guardar el token i fer peticions sense manteniment de sessió - El backend només valida el token sense guardar estat
4.2 APIs REST i Microserveis¶
En arquitectures modernes amb múltiples serveis: - Un token JWT pot ser vàlid en tots els serveis sense sincronització - Cada servei valida independentment el token
4.3 Single Sign-On (SSO)¶
Un usuari pot accedir a múltiples aplicacions amb un sol token JWT, evitant logins repetits.
4.4 Aplicacions Mòbils¶
Les aplicacions mòbils necessiten autenticació lleugera i JWT és perfecte per això.
5. Implementació amb Spring Boot i Angular¶
5.1 Backend: Spring Boot¶
Dependències (Maven)¶
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Classe per Generar i Validar JWT¶
@Component
public class JwtUtils {
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationMs}")
private long jwtExpirationMs;
// Generar JWT
public String generateJwtToken(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(key(), SignatureAlgorithm.HS256)
.compact();
}
// Obtenir usuari del token
public String getUsernameFromJwtToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// Validar token
public boolean validateJwtToken(String authToken) {
try {
Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parseClaimsJws(authToken);
return true;
} catch (SecurityException e) {
System.err.println("Invalid JWT signature: {}" + e);
} catch (MalformedJwtException e) {
System.err.println("Invalid JWT token: {}" + e);
} catch (ExpiredJwtException e) {
System.err.println("Expired JWT token: {}" + e);
} catch (UnsupportedJwtException e) {
System.err.println("Unsupported JWT token: {}" + e);
} catch (IllegalArgumentException e) {
System.err.println("JWT claims string is empty: {}" + e);
}
return false;
}
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
}
Controlador de Login¶
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
String jwt = jwtUtils.generateJwtToken(loginRequest.getUsername());
return ResponseEntity.ok(new JwtResponse(jwt));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Credencials incorrectes");
}
}
}
Filtre per Validar JWT¶
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUsernameFromJwtToken(jwt);
// Establir l'autenticació al context de Spring Security
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
System.err.println("Cannot set user authentication: {}" + e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
Configuració de Spring Security¶
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuthTokenFilter authTokenFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5.2 Frontend: Angular¶
Interceptor de HTTP¶
Els interceptors d'Angular permeten afegir automàticament el token a totes les peticions HTTP:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Obtenir el token emmagatzemat
const token = this.authService.getToken();
if (token) {
// Clonar la petició i afegir el header d'autenticació
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next.handle(req);
}
}
Servei d'Autenticació¶
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = 'http://localhost:8080/api/auth';
private tokenKey = 'auth_token';
private currentUserSubject: BehaviorSubject<any>;
public currentUser: Observable<any>;
constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<any>(
this.getUserFromToken()
);
this.currentUser = this.currentUserSubject.asObservable();
}
// Login
login(username: string, password: string): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/login`, {
username,
password
}).pipe(
tap(response => {
// Guardar el token
localStorage.setItem(this.tokenKey, response.token);
this.currentUserSubject.next(this.getUserFromToken());
})
);
}
// Logout
logout() {
localStorage.removeItem(this.tokenKey);
this.currentUserSubject.next(null);
}
// Obtenir el token emmagatzemat
getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}
// Verificar si l'usuari està autenticat
isAuthenticated(): boolean {
const token = this.getToken();
return token != null;
}
// Decodificar el token per obtenir l'usuari
private getUserFromToken(): any {
const token = this.getToken();
if (token) {
const payload = token.split('.')[1];
const decoded = JSON.parse(atob(payload));
return decoded;
}
return null;
}
}
Component de Login¶
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
username: string = '';
password: string = '';
loading: boolean = false;
submitted: boolean = false;
error: string = '';
constructor(
private authService: AuthService,
private router: Router
) { }
login() {
this.submitted = true;
this.error = '';
this.loading = true;
this.authService.login(this.username, this.password)
.subscribe(
data => {
this.router.navigate(['/dashboard']);
},
error => {
this.error = 'Usuari o contrasenya incorrectes';
this.loading = false;
}
);
}
}
Protector de Rutes (Route Guard)¶
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private authService: AuthService
) { }
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
const isAuthenticated = this.authService.isAuthenticated();
if (isAuthenticated) {
return true;
}
// Redirigir al login
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}
Configuració de Rutes amb Guards¶
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { LoginComponent } from './components/login/login.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard]
},
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Configuració d'Interceptors al Module¶
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { JwtInterceptor } from './interceptors/jwt.interceptor';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: JwtInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
6. Consideracions de Seguretat¶
6.1 Emmagatzematge del Token¶
- localStorage: Fàcil accés per a XSS attacks
- sessionStorage: Similar a localStorage
- Cookies segures: Millor per a HTTPS amb flag
HttpOnly
6.2 Expiració del Token¶
Sempre establir un temps d'expiració curt (15 minuts a 1 hora). Per a sessions més llargues, implementar: - Refresh Tokens: Token separate amb expiració més llarga - Token Renovation: Generar un nou token quan l'anterior expira
6.3 Clau Secreta¶
- Mantenir la clau secreta del costat del servidor
- Usar claus fortes i complexes
- Rotar les claus periodicamente
- Usar variables d'entorn (no commitar al Git)
6.4 HTTPS¶
Always use HTTPS en producció per protegir els tokens durant la transmissió.
6.5 Validació¶
- Sempre validar la signatura del token
- Verificar que no ha expirat
- Validar els claims esperats
7. Comparativa: JWT vs Sessions Tradicionals¶
| Aspecte | JWT | Sessions Tradicionals |
|---|---|---|
| Estat | Stateless | Amb estat (servidor) |
| Escalabilitat | Millor per a distribuïts | Necessita sincronització |
| Mida | Més gran per petició | Més petit |
| Revocació | Difícil fins expiració | Immediata |
| SPAs | Ideal | Menys òptim |
| Microserveis | Perfecte | Complex |
8. Resum¶
JWT és un estàndard modern i segur per a autenticació en aplicacions web contemporànies. Els seus avantatges en escalabilitat i compatibilitat amb SPAs i microserveis el fan la solució ideal per a Angular i Spring Boot. La implementació requereix atenció en seguretat, especialment en:
- Guardar els tokens de forma segura
- Usar HTTPS sempre
- Implementar expiració i refresh tokens
- Protegir la clau secreta
- Validar sempre els tokens al servidor
Amb aquesta estructura, els alumnes tindran una base sòlida per implementar sistemes d'autenticació robustos.