🔐 Tutorial: Servidor d'Autenticació JWT amb Spring Boot¶
Introducció¶
Aquest tutorial explica de manera detallada la implementació d'un servidor d'autenticació basat en JWT (JSON Web Token) utilitzant Spring Boot 3. El projecte és ideal per aprendre els conceptes fonamentals de seguretat en aplicacions web modernes.
Què és JWT?
JWT (JSON Web Token) és un estàndard obert (RFC 7519) que defineix una manera compacta i autònoma de transmetre informació de forma segura entre parts com un objecte JSON. Aquesta informació pot ser verificada i fiable perquè està signada digitalment.
📁 Estructura del Projecte¶
src/
├── main/
│ ├── java/cat/xaviersastre/jwtserver/
│ │ ├── controller/ # Controllers REST
│ │ │ ├── AuthController.java
│ │ │ └── UserController.java
│ │ ├── dto/ # Data Transfer Objects
│ │ │ ├── JwtResponse.java
│ │ │ ├── LoginRequest.java
│ │ │ ├── MessageResponse.java
│ │ │ ├── RegisterRequest.java
│ │ │ ├── UpdateUserRequest.java
│ │ │ └── UserResponse.java
│ │ ├── model/ # Entitats JPA
│ │ │ └── User.java
│ │ ├── repository/ # Repositoris JPA
│ │ │ └── UserRepository.java
│ │ ├── security/ # Configuració de seguretat i JWT
│ │ │ ├── CustomUserDetailsService.java
│ │ │ ├── JwtAuthenticationFilter.java
│ │ │ ├── JwtTokenUtil.java
│ │ │ └── SecurityConfig.java
│ │ ├── service/ # Lògica de negoci
│ │ │ ├── AuthService.java
│ │ │ └── UserService.java
│ │ └── JwtServerApplication.java
│ └── resources/
│ ├── application-postgres.properties
│ └── application.properties
└── test/
🛠️ Tecnologies Utilitzades¶
| Tecnologia | Versió | Descripció |
|---|---|---|
| Spring Boot | 3.1.5 | Framework principal |
| Spring Security | 6.x | Seguretat i autenticació |
| Spring Data JPA | 3.x | Persistència de dades |
| JWT (jjwt) | 0.11.5 | Generació i validació de tokens |
| SQLite/PostgreSQL | - | Base de dades |
| Lombok | 1.18.42 | Reducció de codi boilerplate |
| Java | 17 | Llenguatge de programació |
📦 Dependències (pom.xml)¶
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<groupId>cat.xaviersastre</groupId>
<artifactId>dwes-jwt-server</artifactId>
<version>1.0.0</version>
<name>DWES JWT Authentication Server</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- SQLite JDBC -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.43.0.0</version>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Dependències clau
- spring-boot-starter-security: Proporciona tot el necessari per configurar la seguretat
- jjwt-api/impl/jackson: Llibreria per treballar amb JWT
- spring-boot-starter-validation: Validació de dades d'entrada
⚙️ Configuració (application.properties)¶
# Application settings
spring.application.name=DWES JWT Server
# Server configuration
server.port=8080
# SQLite Database Configuration
spring.datasource.url=jdbc:sqlite:database.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.datasource.username=
spring.datasource.password=
# JPA Configuration
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false
# JWT Configuration
jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
jwt.expiration=86400000
# Logging
logging.level.cat.xaviersastre=DEBUG
logging.level.org.springframework.security=DEBUG
Seguretat del secret
En producció, MAI poseu el jwt.secret directament al fitxer de configuració. Utilitzeu variables d'entorn o un gestor de secrets.
Temps d'expiració
jwt.expiration=86400000 equival a 24 hores en mil·lisegons (24 × 60 × 60 × 1000)
🏗️ Capa de Model (Entity)¶
User.java¶
L'entitat User representa els usuaris del sistema a la base de dades.
package cat.xaviersastre.jwtserver.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String role = "USER";
@Column(nullable = false)
private boolean enabled = true;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
📝 Explicació de les anotacions¶
| Anotació | Descripció |
|---|---|
@Entity |
Indica que aquesta classe és una entitat JPA |
@Table(name = "users") |
Especifica el nom de la taula a la BD |
@Data |
Lombok: genera getters, setters, toString, equals i hashCode |
@NoArgsConstructor |
Lombok: genera constructor sense arguments |
@AllArgsConstructor |
Lombok: genera constructor amb tots els arguments |
@Id |
Marca el camp com a clau primària |
@GeneratedValue |
Generació automàtica de l'ID |
@Column |
Configuració de la columna (unique, nullable, etc.) |
@PrePersist |
Mètode executat abans de persistir l'entitat |
@PreUpdate |
Mètode executat abans d'actualitzar l'entitat |
📂 Capa de Repositori¶
UserRepository.java¶
El repositori proporciona accés a les dades dels usuaris.
package cat.xaviersastre.jwtserver.repository;
import cat.xaviersastre.jwtserver.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
Spring Data JPA
Spring Data JPA genera automàticament la implementació dels mètodes de consulta basant-se en el nom del mètode:
findByUsername→SELECT * FROM users WHERE username = ?existsByUsername→SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)existsByEmail→SELECT EXISTS(SELECT 1 FROM users WHERE email = ?)
📨 Capa de DTOs (Data Transfer Objects)¶
Els DTOs són objectes que serveixen per transferir dades entre capes, separant la representació de les dades de les entitats de la base de dades.
LoginRequest.java¶
package cat.xaviersastre.jwtserver.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
}
RegisterRequest.java¶
package cat.xaviersastre.jwtserver.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class RegisterRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
}
JwtResponse.java¶
package cat.xaviersastre.jwtserver.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class JwtResponse {
private String token;
private String type = "Bearer";
private String username;
private String email;
private String role;
public JwtResponse(String token, String username, String email, String role) {
this.token = token;
this.username = username;
this.email = email;
this.role = role;
}
}
MessageResponse.java¶
package cat.xaviersastre.jwtserver.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class MessageResponse {
private String message;
}
UserResponse.java¶
package cat.xaviersastre.jwtserver.dto;
import lombok.Data;
@Data
public class UserResponse {
private Long id;
private String username;
private String email;
private String role;
private boolean enabled;
public UserResponse(Long id, String username, String email, String role, boolean enabled) {
this.id = id;
this.username = username;
this.email = email;
this.role = role;
this.enabled = enabled;
}
}
UpdateUserRequest.java¶
package cat.xaviersastre.jwtserver.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class UpdateUserRequest {
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@Email(message = "Email must be valid")
private String email;
private String role;
private Boolean enabled;
}
Validació
Les anotacions de validació (@NotBlank, @Size, @Email) permeten validar automàticament les dades d'entrada quan s'utilitza @Valid als controllers.
🔒 Capa de Seguretat¶
JwtTokenUtil.java¶
Aquesta classe és el cor de la gestió JWT. S'encarrega de crear, validar i extreure informació dels tokens.
package cat.xaviersastre.jwtserver.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
🔑 Mètodes principals¶
graph TD
A[generateToken] --> B[createToken]
B --> C[Jwts.builder]
C --> D[Token JWT signat]
E[validateToken] --> F[extractUsername]
E --> G[isTokenExpired]
F --> H[extractAllClaims]
G --> H
| Mètode | Descripció |
|---|---|
generateToken() |
Crea un nou token JWT per a un usuari |
validateToken() |
Valida que el token sigui vàlid i no hagi expirat |
extractUsername() |
Extreu el nom d'usuari del token |
extractExpiration() |
Extreu la data d'expiració del token |
getSigningKey() |
Genera la clau de signatura a partir del secret |
Estructura d'un token JWT
Un token JWT té tres parts separades per punts:
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └── Signature (signatura)
│ └── Payload (càrrega útil - claims)
└── Header (capçalera)
CustomUserDetailsService.java¶
Implementació del servei UserDetailsService de Spring Security que carrega els usuaris des de la base de dades.
package cat.xaviersastre.jwtserver.security;
import cat.xaviersastre.jwtserver.model.User;
import cat.xaviersastre.jwtserver.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())))
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(! user.isEnabled())
.build();
}
}
UserDetailsService
Aquesta interfície és utilitzada per Spring Security per carregar les dades de l'usuari durant el procés d'autenticació. El mètode loadUserByUsername es crida automàticament quan un usuari intenta iniciar sessió.
JwtAuthenticationFilter.java¶
Filtre que intercepta totes les peticions HTTP i verifica el token JWT.
package cat.xaviersastre.jwtserver.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
// 1. Obtenir el header Authorization
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 2. Verificar si el header conté un token Bearer
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtTokenUtil.extractUsername(jwt);
} catch (Exception e) {
logger.error("JWT Token extraction error: " + e.getMessage());
}
}
// 3. Si tenim un username i no hi ha autenticació prèvia
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 4. Validar el token
if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 5. Establir l'autenticació al context de seguretat
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
// 6. Continuar amb la cadena de filtres
chain.doFilter(request, response);
}
}
🔄 Flux del filtre¶
sequenceDiagram
participant Client
participant Filter as JwtAuthenticationFilter
participant JwtUtil as JwtTokenUtil
participant UserService as CustomUserDetailsService
participant SecurityContext
Client->>Filter: HTTP Request (Authorization: Bearer xxx)
Filter->>Filter: Extreure token del header
Filter->>JwtUtil: extractUsername(token)
JwtUtil-->>Filter: username
Filter->>UserService: loadUserByUsername(username)
UserService-->>Filter: UserDetails
Filter->>JwtUtil: validateToken(token, userDetails)
JwtUtil-->>Filter: true/false
Filter->>SecurityContext: setAuthentication(authToken)
Filter->>Client: Continuar amb la petició
SecurityConfig.java¶
Configuració central de Spring Security.
package cat.xaviersastre.jwtserver.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailsService customUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Desactivar CSRF (no necessari per APIs REST amb JWT)
.csrf(csrf -> csrf.disable())
// Configurar autoritzacions
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/auth/register", "/error").permitAll()
.anyRequest().authenticated()
)
// Configurar sessió sense estat (stateless)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Afegir provider d'autenticació
.authenticationProvider(authenticationProvider())
// Afegir filtre JWT abans del filtre d'autenticació per defecte
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
📋 Components de la configuració¶
| Component | Funció |
|---|---|
PasswordEncoder |
Codifica les contrasenyes amb BCrypt |
AuthenticationManager |
Gestiona el procés d'autenticació |
DaoAuthenticationProvider |
Proveïdor d'autenticació basat en DAO |
SecurityFilterChain |
Cadena de filtres de seguretat |
CSRF desactivat
Desactivem CSRF perquè les APIs REST amb autenticació basada en tokens no són vulnerables a atacs CSRF, ja que el token s'ha d'incloure explícitament a cada petició.
SessionCreationPolicy.STATELESS
Configurem la sessió com a STATELESS perquè no volem que Spring Security mantingui sessions HTTP. Cada petició s'autentica independentment mitjançant el token JWT.
💼 Capa de Serveis¶
AuthService.java¶
Servei que gestiona l'autenticació i el registre d'usuaris.
package cat.xaviersastre.jwtserver.service;
import cat.xaviersastre.jwtserver.model.User;
import cat.xaviersastre.jwtserver.repository.UserRepository;
import cat.xaviersastre.jwtserver.security.JwtTokenUtil;
import cat.xaviersastre.jwtserver.dto.JwtResponse;
import cat.xaviersastre.jwtserver.dto.LoginRequest;
import cat.xaviersastre.jwtserver.dto.MessageResponse;
import cat.xaviersastre.jwtserver.dto.RegisterRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenUtil jwtTokenUtil;
private final AuthenticationManager authenticationManager;
public JwtResponse login(LoginRequest request) {
// 1. Autenticar l'usuari
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
// 2. Obtenir els detalls de l'usuari autenticat
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 3. Generar el token JWT
String token = jwtTokenUtil.generateToken(userDetails);
// 4. Obtenir informació addicional de l'usuari
User user = userRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new RuntimeException("User not found"));
// 5. Retornar la resposta amb el token
return new JwtResponse(token, user.getUsername(), user.getEmail(), user.getRole());
}
public MessageResponse register(RegisterRequest request) {
// 1. Verificar que el username no existeixi
if (userRepository.existsByUsername(request.getUsername())) {
throw new RuntimeException("Username already exists");
}
// 2. Verificar que l'email no existeixi
if (userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("Email already exists");
}
// 3. Crear nou usuari
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword())); // Important: codificar la contrasenya!
user.setEmail(request.getEmail());
user.setRole("USER");
user.setEnabled(true);
// 4. Guardar l'usuari
userRepository.save(user);
return new MessageResponse("User registered successfully");
}
}
🔐 Flux de login¶
sequenceDiagram
participant Client
participant AuthService
participant AuthManager as AuthenticationManager
participant JwtTokenUtil
participant UserRepo as UserRepository
Client->>AuthService: login(username, password)
AuthService->>AuthManager: authenticate(credentials)
AuthManager-->>AuthService: Authentication object
AuthService->>JwtTokenUtil: generateToken(userDetails)
JwtTokenUtil-->>AuthService: JWT Token
AuthService->>UserRepo: findByUsername(username)
UserRepo-->>AuthService: User entity
AuthService-->>Client: JwtResponse(token, user info)
UserService.java¶
Servei que gestiona les operacions CRUD d'usuaris.
package cat.xaviersastre.jwtserver.service;
import cat.xaviersastre.jwtserver.dto.UpdateUserRequest;
import cat.xaviersastre.jwtserver.dto.UserResponse;
import cat.xaviersastre.jwtserver.model.User;
import cat.xaviersastre.jwtserver.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public List<UserResponse> getAllUsers() {
return userRepository.findAll().stream()
.map(this::convertToUserResponse)
.collect(Collectors.toList());
}
public UserResponse getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
return convertToUserResponse(user);
}
public UserResponse updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
// Actualitzar username si es proporciona
if (request.getUsername() != null && !request.getUsername().isEmpty()) {
if (! user.getUsername().equals(request.getUsername())
&& userRepository.existsByUsername(request.getUsername())) {
throw new RuntimeException("Username already exists");
}
user.setUsername(request.getUsername());
}
// Actualitzar email si es proporciona
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
if (!user.getEmail().equals(request.getEmail())
&& userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("Email already exists");
}
user.setEmail(request.getEmail());
}
// Actualitzar password si es proporciona (codificar!)
if (request.getPassword() != null && !request.getPassword().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
// Actualitzar role si es proporciona
if (request.getRole() != null && !request.getRole().isEmpty()) {
user.setRole(request.getRole());
}
// Actualitzar enabled si es proporciona
if (request.getEnabled() != null) {
user.setEnabled(request.getEnabled());
}
User updatedUser = userRepository.save(user);
return convertToUserResponse(updatedUser);
}
public void deleteUser(Long id) {
if (! userRepository.existsById(id)) {
throw new RuntimeException("User not found with id: " + id);
}
userRepository.deleteById(id);
}
private UserResponse convertToUserResponse(User user) {
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getRole(),
user.isEnabled()
);
}
}
🌐 Capa de Controllers¶
AuthController.java¶
Controller que gestiona els endpoints d'autenticació.
package cat.xaviersastre.jwtserver.controller;
import cat.xaviersastre.jwtserver.dto.JwtResponse;
import cat.xaviersastre.jwtserver.dto.LoginRequest;
import cat.xaviersastre.jwtserver.dto.MessageResponse;
import cat.xaviersastre.jwtserver.dto.RegisterRequest;
import cat.xaviersastre.jwtserver.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public ResponseEntity<JwtResponse> login(@Valid @RequestBody LoginRequest request) {
try {
JwtResponse response = authService.login(request);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/register")
public ResponseEntity<MessageResponse> register(@Valid @RequestBody RegisterRequest request) {
try {
MessageResponse response = authService.register(request);
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(new MessageResponse(e.getMessage()));
}
}
}
UserController.java¶
Controller que gestiona els endpoints d'usuaris (requereix autenticació).
package cat.xaviersastre.jwtserver.controller;
import cat.xaviersastre.jwtserver.dto.MessageResponse;
import cat.xaviersastre.jwtserver.dto.UpdateUserRequest;
import cat.xaviersastre.jwtserver.dto.UserResponse;
import cat.xaviersastre.jwtserver.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<UserResponse>> getAllUsers() {
List<UserResponse> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
try {
UserResponse user = userService.getUserById(id);
return ResponseEntity.ok(user);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
try {
UserResponse user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<MessageResponse> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.ok(new MessageResponse("User deleted successfully"));
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
}
🚀 Classe Principal¶
JwtServerApplication.java¶
package cat.xaviersastre.jwtserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JwtServerApplication {
public static void main(String[] args) {
SpringApplication.run(JwtServerApplication.class, args);
}
}
📡 API Endpoints¶
Endpoints d'Autenticació (públics)¶
| Mètode | Endpoint | Descripció |
|---|---|---|
| POST | /api/auth/register |
Registrar nou usuari |
| POST | /api/auth/login |
Iniciar sessió i obtenir token |
Endpoints d'Usuaris (requereixen autenticació)¶
| Mètode | Endpoint | Descripció |
|---|---|---|
| GET | /api/users |
Obtenir tots els usuaris |
| GET | /api/users/{id} |
Obtenir usuari per ID |
| PUT | /api/users/{id} |
Actualitzar usuari |
| DELETE | /api/users/{id} |
Eliminar usuari |
🧪 Exemples d'ús amb cURL¶
Registrar un usuari¶
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "alumne",
"password": "password123",
"email": "alumne@escola.cat"
}'
Resposta:
{
"message": "User registered successfully"
}
Iniciar sessió¶
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "alumne",
"password": "password123"
}'
Resposta:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"type": "Bearer",
"username": "alumne",
"email": "alumne@escola.cat",
"role": "USER"
}
Obtenir tots els usuaris (amb token)¶
curl -X GET http://localhost:8080/api/users \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Actualitzar un usuari¶
curl -X PUT http://localhost:8080/api/users/1 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{
"email": "nou_email@escola.cat"
}'
📊 Diagrama d'Arquitectura¶
graph TB
subgraph Client
A[Aplicació Client]
end
subgraph "Spring Boot Application"
subgraph Controllers
B[AuthController]
C[UserController]
end
subgraph Services
D[AuthService]
E[UserService]
end
subgraph Security
F[SecurityConfig]
G[JwtAuthenticationFilter]
H[JwtTokenUtil]
I[CustomUserDetailsService]
end
subgraph Repository
J[UserRepository]
end
subgraph Model
K[User Entity]
end
end
subgraph Database
L[(SQLite/PostgreSQL)]
end
A -->|HTTP Request| B
A -->|HTTP Request + JWT| C
B --> D
C --> E
D --> H
D --> J
E --> J
G --> H
G --> I
I --> J
J --> K
K --> L
F --> G
Resum¶
- Model: Defineix l'estructura de dades (User entity)
- Repository: Proporciona accés a la base de dades
- DTOs: Transferència de dades entre capes
- Security: Gestió de JWT i configuració de Spring Security
- Services: Lògica de negoci
- Controllers: Endpoints REST
Success
Ara ja pot entendre com funciona un servidor d'autenticació JWT amb Spring Boot. Practica modificant el codi i afegint noves funcionalitats!