f in x
Spring Security con JWT e OAuth2: autenticazione moderna per le tue API
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Sviluppo di siti web

Spring Security con JWT e OAuth2: autenticazione moderna per le tue API

[2026-06-10] Author: Ing. Calogero Bono

Hai un backend Java con Spring Boot, e i tuoi client — app mobile, SPA React, servizi esterni — devono autenticarsi in modo sicuro. Hai letto di JWT e OAuth2, ma quando apri la documentazione ufficiale ti trovi davanti a una selva di configurazioni e termini che sembrano fatti apposta per confonderti. Il risultato? Troppe finiscono per esporre endpoint, lasciare token senza scadenza o, peggio, copiare incollando pezzi di codice da Stack Overflow senza capire se funzionano davvero.

Noi, di Meteora Web, lavoriamo ogni giorno con stack Java e Spring Boot sui progetti dei nostri clienti. Veniamo da otto anni di gestione di sistemi reali — dall'ERP di un negozio di abbigliamento alla piattaforma proprietaria per la gestione social di più aziende. Abbiamo visto che un sistema di autenticazione mal configurato costa molto più del tempo speso per farlo bene: significa dati esposti, fiducia persa, e in alcuni casi multe salate. Per questo abbiamo deciso di scrivere questa guida operativa, focalizzata su Spring Security con JWT e OAuth2, senza giri di parole e con esempi che puoi copiare e testare oggi stesso.

Perché JWT e OAuth2 insieme? Una scelta che paga

La domanda giusta è: perché non bastano uno username e una password con sessione server-side? Se hai un'applicazione monolitica tradizionale, forse sì. Ma appena entrano in gioco più client (mobile, web, API esterne) o un'architettura a microservizi, la gestione delle sessioni diventa un collo di bottiglia. JWT (JSON Web Token) ti permette di avere un token auto-contenuto: contiene i dati dell'utente (claims) e una firma che ne garantisce l'integrità. OAuth2, invece, è il protocollo che standardizza come emettere e validare questi token tra diverse parti (client, resource server, authorization server).

La combinazione è vincente perché:

  • Stateless: il server non deve memorizzare la sessione. Ogni richiesta porta con sé tutto ciò che serve per essere autenticata.
  • Scalabile: puoi aggiungere istanze del backend senza dover condividere un database di sessioni.
  • Interoperabile: qualsiasi client (JavaScript, mobile, servizio) può usare lo stesso token se rispetta lo standard.
  • Delegabile: OAuth2 permette di delegare l'autenticazione a provider esterni (Google, GitHub, un tuo Identity Provider).

Dal punto di vista economico, implementarlo male significa perdere clienti. Noi lo vediamo ogni giorno: se il login non funziona, se il token scade senza preavviso, l'utente abbandona. E un utente perso costa più di un'ora di sviluppo.

Sponsored Protocol

Setup di base: le dipendenze giuste

Partiamo dal presupposto che hai un progetto Spring Boot (qualsiasi versione 3.x, che usa Spring Security 6). La prima cosa da fare è aggiungere le dipendenze. Noi usiamo Maven, ma il concetto è identico per Gradle.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<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>

La libreria jjwt è quella che usiamo nei nostri progetti per gestire la creazione e validazione dei JWT. È matura, ben mantenuta e non ha dipendenze pesanti. Per OAuth2 Resource Server, Spring Boot ci mette a disposizione un supporto nativo: basta la dipendenza oauth2-resource-server e configurare la decodifica del token tramite chiave pubblica o endpoint di introspecting.

Configurazione di Spring Security: SecurityFilterChain

In Spring Security 6, la configurazione è basata su un bean SecurityFilterChain. Dimentica le vecchie WebSecurityConfigurerAdapter. Ecco il nostro setup standard per una API REST che usa JWT.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, 
                          AuthenticationProvider authenticationProvider) {
        this.jwtAuthFilter = jwtAuthFilter;
        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // API stateless -> CSRF non serve
            .cors(Customizer.withDefaults()) // abilita CORS per richieste cross-origin
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/", "/api/public/").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Tre cose cruciali, tutte spesso trascurate nei progetti che ci arrivano da rivedere:

Sponsored Protocol

  • CSRF disabilitato: per API stateless è corretto, ma mai per app che gestiscono sessioni tramite cookie.
  • Session stateless: Spring Security non deve creare una sessione HTTP; altrimenti vanifichi il vantaggio del JWT.
  • Filtro prima di UsernamePasswordAuthenticationFilter: il tuo filtro custom legge il token e imposta l'Authentication nel SecurityContextHolder.

Il filtro JWT: cuore della validazione

Scriviamo un filtro che estrae il token dall'header Authorization: Bearer <token>, lo valida e popola il contesto di sicurezza. Ecco la versione operativa che usiamo nei nostri progetti:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain)
            throws ServletException, IOException {
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        final String jwt = authHeader.substring(7);
        final String userEmail = jwtService.extractUsername(jwt);

        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                    );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

L'errore più comune che correggiamo nei progetti: non controllare che l'Authentication sia già presente (evita di riprocessare lo stesso token su più filtri) e non gestire correttamente le eccezioni di token scaduto. Un JWT scaduto non deve far rispondere con un 500, ma con un 401 chiaro.

Sponsored Protocol

JwtService: generazione e validazione

Il service che firma e verifica i token. Noi usiamo una chiave segreta HMAC (256 bit) per ambienti di sviluppo e una coppia RSA per produzione. Ecco un esempio con HMAC:

@Service
public class JwtService {

    private static final String SECRET_KEY = System.getenv("JWT_SECRET"); // mai hardcoded!

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public String generateToken(UserDetails userDetails) {
        return buildToken(new HashMap<>(), userDetails);
    }

    private String buildToken(Map extraClaims, UserDetails userDetails) {
        return Jwts.builder()
                .claims(extraClaims)
                .subject(userDetails.getUsername())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 ore
                .signWith(getSignInKey())
                .compact();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private 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.parser()
                .verifyWith(getSignInKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    private SecretKey getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

Nota: la chiave segreta va sempre presa da una variabile d'ambiente o da un vault. Nei nostri audit di sicurezza, il 60% dei progetti che esaminiamo ha la chiave hardcodata nel codice. È la prima cosa che controlliamo.

Sponsored Protocol

OAuth2 Resource Server con JWT nativo

Spring Boot supporta OAuth2 Resource Server in modo quasi dichiarativo. Se il tuo token JWT è emesso da un provider esterno (es. Google, Auth0, Keycloak), puoi evitare del tutto il filtro custom:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://accounts.google.com
          jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs

Oppure, se gestisci tu stesso l'emissione (autorizzazione centralizzata), puoi configurare una chiave pubblica RSA nel file di properties e Spring validerà la firma automaticamente. Noi preferiamo questa strada quando abbiamo un backend monolitico con un proprio dominio di utenti, perché ci dà il controllo totale senza dipendere da un provider esterno.

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(publicKey).build();
}

Il vantaggio è che non devi scrivere il filtro: Spring Security lo fa per te, basandosi sulla configurazione del Resource Server. Questo riduce codice e potenziali bug.

Refresh token e logout: la gestione che salva l'esperienza utente

Un JWT ha una scadenza breve (tipicamente 15 minuti a 1 ora). Per non far reinserire le credenziali ogni volta, si usa un refresh token con durata più lunga (es. 7 giorni). Il refresh token deve essere memorizzato lato server (es. in database) e revocabile. Quando scade l'access token, il client chiama un endpoint /api/auth/refresh con il refresh token, e ottiene un nuovo access token.

Implementare il refresh è semplice: nel tuo controller di autenticazione, dopo aver verificato le credenziali, emetti entrambi i token. Poi, per il refresh, ricevi il refresh token, lo cerchi nel DB, controlli che non sia scaduto o revocato, e generi un nuovo access token (e opzionalmente un nuovo refresh token).

Sponsored Protocol

Per il logout, la strategia migliore è invalidare il refresh token (cancellarlo dal DB o marcare come revocato). L'access token, essendo stateless, non può essere revocato fino alla sua scadenza naturale. Per mitigare il rischio, usa una blacklist in memoria (Redis) dei token scaduti/logout, controllata nel filtro JWT. Non è perfetta ma è pratica.

Sicurezza avanzata: errori da evitare e best practice

Abbiamo visto troppe implementazioni che dimenticano:

  • Validare l'audience: se il tuo servizio A emette token per il servizio B, il token deve contenere l'audience corretta. Altrimenti un token rubato può essere usato su risorse non autorizzate.
  • Non esporre informazioni sensibili nei claims: mai mettere password, ruoli in chiaro se non necessari, o dati personali non necessari.
  • Limitare la dimensione del token: un JWT troppo grande rallenta ogni richiesta. Inserisci solo i claim essenziali.
  • Usare HTTPS ovunque: se passi il token in chiaro su HTTP, è come scrivere la password su un post-it.
  • Rate limiting sugli endpoint di login e refresh: per prevenire brute force. Spring Boot offre Bucket4j o puoi integrare un gateway come NGINX.

Noi, di Meteora Web, abbiamo un approccio pratico: ogni volta che implementiamo un sistema di autenticazione, simuliamo un attacco di replay e rotazione dei token. È un investimento che ripaga subito: un breach costa in media 4,5 milioni di dollari, secondo IBM. Per una PMI, basta un attacco per chiudere.

In sintesi — cosa fare adesso

  1. Aggiungi le dipendenze a Spring Security e jjwt nel tuo pom.xml.
  2. Configura il SecurityFilterChain con policy stateless, CORS e filtro JWT.
  3. Implementa JwtService e JwtAuthenticationFilter come sopra, testandoli con un endpoint protetto.
  4. Aggiungi refresh token e logout per un'esperienza utente fluida.
  5. Controlla che la chiave segreta sia in una variabile d'ambiente e non nel codice.
  6. Abilita HTTPS e rate limiting sugli endpoint sensibili.
  7. Se usi un provider esterno, sfrutta il supporto nativo di OAuth2 Resource Server.

Se stai iniziando ora, parti dal repository ufficiale di Spring Security con JWT: esempi su GitHub. Ma non fermarti lì: adatta la configurazione al tuo dominio, ai tuoi utenti, ai tuoi volumi. Se hai un progetto concreto e vuoi una revisione, contattaci: a noi piace partire dai numeri e dai rischi reali, non dalla teoria.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // WEB AGENCY

Costruiamo la presenza digitale che la tua azienda merita.

Siti web, social, pubblicità online, e-commerce e hosting performante: ingegnerizzati con metodo da ingegneri informatici a Sciacca, per tutta Italia.

> MW_JOURNAL

> READ_ALL()