Salta el contingut

🔐 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:

  • findByUsernameSELECT * FROM users WHERE username = ?
  • existsByUsernameSELECT EXISTS(SELECT 1 FROM users WHERE username = ?)
  • existsByEmailSELECT 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

  1. Model: Defineix l'estructura de dades (User entity)
  2. Repository: Proporciona accés a la base de dades
  3. DTOs: Transferència de dades entre capes
  4. Security: Gestió de JWT i configuració de Spring Security
  5. Services: Lògica de negoci
  6. 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!