Immagine descrittiva del post

In questo articolo vedrai come utilizzare Spring Security e lo standard JWT per la sicurezza delle API REST all’interno di un’applicazione Spring Boot.

Se non lo hai ancora fatto, leggi l’articolo in cui ti spiego come creare API RESTful con Spring Boot.

 

Hai mai sentito parlare di JWT? No? Non preoccuparti.

Prima di cominciare vediamo brevemente cos’è un JWT (Json Web Token) e come questo viene utilizzato nelle applicazioni web.

 

JSON Web Token

JSON Web Token è uno standard di internet utilizzato per autenticare le richieste ai Web Services nello scambio di informazioni tra Client e Server.

Un esempio è il protocollo Oauth2. Il Client richiede una risorsa ad un Web Service inviando nella richiesta un access_token.

Un token con lo standard JWT ha una rappresentazione JSON e può contenere informazioni personalizzate. La sua struttura è composta da un header, un payload (un set di informazioni chiamati Claims) e una signature. Un JWT infatti, può essere firmato tramite un algoritmo di cifratura come SHA256.

Con questa breve introduzione, possiamo ora formalizzare il concetto di JWT all’interno di un’applicazione Spring.

 

Cosa vedrai continuando la lettura

Un utente che effettua il login attraverso un client riceve in cambio un token JWT generato dal server, ovvero dalla nostra applicazione Spring Boot.

Vedrai come creare un’API di login per autenticare l’utente e come generare un token JWT codificato con chiave segreta contenente informazioni.

Ogni richiesta di un client alla nostra applicazione (server) deve contenere nell’Header il token di autenticazione.

Vedrai come decodificare il token JWT ricevuto per autorizzare la richiesta.

Pronto? Iniziamo.

 

Utilizzare Spring Security con JWT per la sicurezza delle API

Per utilizzare Spring Security con meccanismo Oauth2 all’interno del nostro progetto Spring Boot occorre eseguire una corretta configurazione.

Seguiremo per lo sviluppo i seguenti passaggi:

  1. Aggiungere le dipendenze Maven
  2. Configurare Spring Security
  3. Definire API REST per l’autenticazione

 

#1 Aggiungere le dipendenze Maven

Il primo passo consiste nell’aggiungere Spring Security al progetto utilizzando la dipendenza Maven fornita dallo starter Spring.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Oltre a quella appena vista, aggiungiamo le dipendenze java-jwt e joda-time, ci serviranno nell’implementazione.

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.3</version>
</dependency>

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
</dependency>

Se conosci altre librerie che portano allo stesso risultato puoi sostituirle con quelle appena viste. In questo caso puoi aggiungere un commento al post così, sia io che altri lettori, possiamo imparare 😉

 

#2 Configurare Spring Security

In questo fase andremo a creare le 3 classi Java che ci serviranno per implementare la sicurezza delle API:

  • Un componente JwtProvider per la gestione del token JWT
  • Una classe AuthorizationFilter che estende BasicAuthenticationFilter: rappresenta il filtro che viene eseguito ad ogni chiamata HTTP in ingresso
  • Una classe di configurazione SecurityConfig che estende WebSecurityConfigurerAdapter
 

Aggiungere le proprietà

Prima di tutto aggiungiamo al file application.properties le proprietà che andremo ad utilizzare all’interno delle classi, ovvero security.secret, security.prefix e security.param. Rappresentano rispettivamente la chiave segreta per la codifica, il prefisso della stringa di autorizzazione e il nome del parametro presente nell’header della richiesta. Più avanti capirai il loro utilizzo.

security.secret=chiavesupersegretissima
security.prefix='Bearer '
security.param=Authorization
 

La classe JwtProvider (utilità)

Vediamo ora nel dettaglio quanto detto sopra partendo dalla creazione della classe JwtProvider.

/**
 * The type Jwt provider.
 */
@Component
@Slf4j
public class JwtProvider {

    @Value("${security.secret}")
    private String secret;

    @Value("${security.prefix}")
    private String prefix;

    public String createJwt() {
        return JWT.create()
            .withSubject("subject")
            .withIssuer("issuer")
            .withIssuedAt(DateTime.now().toDate())
            .withClaim("someClaim", "someClaimDesc")
            .withExpiresAt(DateTime.now().plusMonths(1).toDate())
            .sign(Algorithm.HMAC256(secret));
    }

    public DecodedJWT decodeJwt(String jwt) {
        try {
            jwt = jwt.replace(prefix, "").trim();
            return JWT.require(Algorithm.HMAC256(secret)).build().verify(jwt);
        } catch (Exception e) {
            log.error("Invalid JWT", e);
        }
        return null;
    }
}

La classe implementa due metodi. Il primo, createJwt, serve, come suggerisce il nome, per generare un nuovo token JWT. Il secondo metodo, decodeJwt, verifica la validità decodificandolo. Importante annotare la classe come Component per essere utilizzata da altri componenti.

Entrambi i metodi utilizzano l’oggetto JWT della libreria auth0 inserita poco fa, mentre viene fatto uso dell’oggetto DateTime fornito dalla libreria joda-time per la gestione delle date come la data di scadenza.

Il token contiene nella sua rappresentazione alcune informazioni. L’oggetto JWT contiene alcuni metodi per l’inserimento di queste informazioni all’interno del token. Si distinguono dal prefisso with (.withSubject, .withClaim, …), in particolare, si può aggiungere qualsiasi informazione personalizzata all’interno di un Claim tramite il metodo .withClaim/s.

Il JWT viene in fine firmato con l’algoritmo HMAC256 e chiave segreta letta dal file di properties, utilizzati anche nella decodifica.

 

Il parametro prefix, letto anch’esso dalle proprietà, rappresenta il prefisso della stringa contenente il token ed è una caratteristica del meccanismo Oauth2. Ad esempio, nell’header della richiesta possiamo trovare un’autenticazione di tipo Bearer: Bearer eyJhbGciOiJIUzI1Ni….resto del jwt… .

 

La classe AuthorizationFilter (filtro)

La seconda classe che andremo a creare rappresenta il filtro di autorizzazione che viene attivato automaticamente (da inserire nella configurazione che vedremo tra poco) quando viene ricevuta una richiesta da un client.

Questa, estende la classe BasicAuthenticationFilter e implementa il metodo ereditato doFilterInternal. Una sua implementazione è la seguente:

/**
 * The type Authorization filter.
 */
public class AuthorizationFilter extends BasicAuthenticationFilter {

    private JwtProvider jwtProvider;
    private String prefix;
    private String param;

    @Autowired
    public AuthorizationFilter(AuthenticationManager authenticationManager, JwtProvider jwtProvider, String prefix, String param) {
        super(authenticationManager);
        this.jwtProvider = jwtProvider;
        this.prefix = prefix;
        this.param = param;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader(param);
        if (header == null || !header.startsWith(prefix)) {
            chain.doFilter(req, res);
            return;
        }
        UsernamePasswordAuthenticationToken authentication = this.getAuthentication(header);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(String header) {
        DecodedJWT decoded = jwtProvider.decodeJwt(header);
        return new UsernamePasswordAuthenticationToken(decoded.getSubject(), null, Collections.emptyList());
    }
}

Nel costruttore della classe vengono passati come parametri l’oggetto JwtProvider visto poco fa, lo utilizzeremo per decodificare il token, e le proprietà del file application.properties che servono al metodo doFilterInternal.

Esaminiamo la funzione doFilterInternal, il cuore della classe.

Il metodo viene appunto ereditato dalla classe BasicAuthenticationFilter, e ha come parametri la richiesta (req), la risposta (res) e la filter chain (chain). Quello che faremo è verificare il token presente nella richiesta e decidere come proseguire nella catena dei filtri.

Per prima cosa si verifica la presenza all’interno dell’header della richiesta del parametro Authorization, e che questo sia con prefisso Bearer. Nel caso di esito positivo proseguiremo con la decodifica del token, viceversa, otterremmo nella richiesta http fatta dal client un errore di autenticazione. Se anche la decodifica ha esito positivo il contesto sarà arricchito dal nuovo elemento di autenticazione UsernamePasswordAuthenticationToken, senza il quale la FilterChain, alla fine del suo percorso, restituirebbe comunque un errore di autenticazione. La chiamata doFilter della FilterChain permette di proseguire nella catena e completare l’autorizzazione.

Bene, abbiamo creato il nostro filtro che permette di autorizzare una richiesta esterna tramite token JWT presente nell’header realizzando cosi la sicurezza delle API.

Ma, funziona?

Assolutamente No! Il filtro viene ignorato finché non lo aggiungiamo all’interno della catena dei filtri (FilterChain) di Spring Security e lo faremo creando una configurazione appropriata.

 

La classe SecurityConfig (configurazione)

/**
 * The type Security config.
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtProvider jwtProvider;

    @Value("${security.prefix}")
    private String prefix;
    
    @Value("${security.param}")
    private String param;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and().csrf().disable();
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilter(
                        new AuthorizationFilter(authenticationManager(), jwtProvider, prefix, param)
                )
                .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated();
    }
}

La classe SecurityConfig qui sopra rappresenta la nostra configurazione per Spring Security.

Cerchiamo di capire meglio quanto scritto.

SecurityConfig viene annotata con @Configuration per rappresentare una configurazione Spring, e altre due annotazioni @EnableWebSecurity e @EnableGlobalMethodSecurity riguardanti Spring Security.

Per abilitare Http Security in Spring occorre estendere la classe astratta WebSecurityConfigurerAdapter e implementare il metodo configure.

Per fornire una configurazione di default dobbiamo agire sull’oggetto HttpSecurity passato come argomento nel metodo configure, come mostrato nell’esempio proposto.

Per essere sicuri che ogni richiesta sia autenticata basta aggiungere:

http.authorizeRequests().anyRequest().authenticated();

Nella configurazione proposta, abbiamo permesso a tutte le richieste che iniziano con endpoint /public di essere escluse dalla catena di filtri di sicurezza. Riguardano le API che vogliamo esporre pubblicamente, come ad esempio la registrazione dell’utente e il login.

.antMatchers("/public/**").permitAll()

Per implementare Spring Security con JWT dobbiamo prendere in considerazione alcune osservazioni.

Di default, Spring Security utilizza un sistema di generazione cookie che vengono scambiati ad ogni richiesta client-server, e registra un utente autenticato tra le sessioni attive di Spring in un oggetto chiamato Principal. Questo, insieme alla filter chain (catena dei filtri di sicurezza) di default, permette a Spring di verificare l’autenticazione dell’utente controllando le informazioni tra le sessioni attive.

Nella nostra implementazione JWT dobbiamo quindi:

  • escludere i cookie
  • non creare delle sessioni (tramite sessioni con politica stateless)
  • modificare il comportamento della filter chain per validare il token

Abbiamo fatto questo aggiungendo alla configurazione le righe:

.cors().and().csrf().disable()

.sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

Abbiamo anche aggiunto il filtro AuthorizationFilter estendendo il filtro di default BasicAuthenticationFilter visto poco fa.

.addFilter(new AuthorizationFilter(jwtProvider, prefix, param)

Cosi facendo, abbiamo configurato correttamente Spring Security per gestire un’autenticazione con JWT.

 

#3 Definire una API per l'autenticazione

Guardiamo ora un esempio di API di autenticazione per la generazione del JWT da parte del server.

 

Le classi DTO

public class LoginInputDto {     
private String username;
private String password;
// getter and setter
}
public class LoginOutputDto {
private String token;

// getter and setter
}
 

API di login

@RestController 
@RequestMapping("public/authentication") 
public class AuthenticationController { 

    @Autowired 
    private JwtProvider jwtProvider; 
    
    @Autowired 
    private UserService userService; 
    
    @PostMapping 
    public ResponseEntity<LoginOutputDto> authenticate(@RequestBody LoginInputDto body) { 
        // TODO verifica se l'utente è registrato su db altrimenti restituisci status 401 
        
        // FIXME aggiungi informazioni nel JWT modificando il metodo createJwt(..info). 
        String jwt = jwtProvider.createJwt(); 
        LoginOutputDto dto = new LoginOutputDto(); 
        dto.setToken(jwt); 
        return ResponseEntity.ok(dto); 
    } 
}

All’interno di authenticate dovrebbe essere inserito un controllo sull’utente (è presente nel database?). Al fine dell’esempio, escludiamo questo controllo restituendo direttamente l’oggetto LoginOutputDto contenente il token JWT generato e valido per le successive chiamate.

L’API di registrazione non è altro che un endpoint in cui vengono passati i dati dell’utente (di solito tramite un form di registrazione) per essere salvati sul database.

 

Arrivato a questo punto, avrai configurato correttamente Spring Security per la gestione di un’autenticazione con JWT creando un’API per il login dell’utente, contribuendo cosi alla sicurezza delle API della tua applicazione Spring Boot.

Ben fatto!

 

Prossimi passi:

 

Inoltre, se vuoi rimanere aggiornato sulla pubblicazione di nuovi articoli, segui la pagina Facebook di laterale.cloud.

Recommended Posts

6 Comments

  1. Ciao Alessandro,
    grazie per la tua belle spiegazione. Ho implementato tutto ma chiamando un servizioo con Postman, mi dice giustamente FORBIDDEN perche non so come specificare il token
    io ho aggiunto nell’header della chiamata:
    Key Value
    Authorization Bearer d0ac23e7-f112-4056-b304-4ceba1c2ed7d

    Ma dove prendo il valore del token da inserire? Questo token non risulta valito , l’ho preso dalle tracce di springboot quando parte
    Using generated security password: 96553a2d-42a2-44ed-8679-7ed390b8054
    Grazie
    Andrea

    • Ciao Andrea, devi prima poter generare un token JWT creando, ad esempio, una API di login.
      In pratica, ti basta una API che restituisca il risultato dell’operazione JwtProvider.createJwt(). Assicurati che questa sia raggiungibile pubblicamente, ad esempio /public/authentication, in base alla tua configurazione (es: .antMatchers("/public/**").permitAll()…).

      Se può aiutarti, guarda questo esempio pubblicato su github.

  2. Ciao Alessandro, ho un problema magari avr? sbagliato qualcosa, ma a me effettua per ogni request due chiamate al metodo doFilterInternal e mi entra due volte nel controller

    • Ciao Davide, se hai configurato tutto correttamente non dovrebbe dipendere dall’applicazione ma piuttosto dal client che utilizzi per chiamare la tua API. Per esempio, la chiami da Chrome? in questo caso puoi provare con Postman o curl da riga di comando.

  3. Do you have any video of that? I’d love to find out some additional information.

    • Hi, thanks for the comment.
      I haven’t published any videos for now, I think I’ll do it in the future.
      In the meantime, you can check out code on github at this address if it helps you.


Add a Comment

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *