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

Versione YAML:

security:
  secret: chiavesupersegretissima
  prefix: 'Bearer '
  param: Authorizationn
 

La classe JwtProvider (utilità)

Premessa. All’interno delle prossime due classi che vedrai, sono presenti dei riferimenti alle proprietà secret, prefix e param tramite la classe di configurazione SecurityConfig decritta successivamente che ne estrae il valore dal file properties assegnandolo a tre attributi statici della classe, ad esempio SecurityConfig.secret. Pertanto, dovrai aver completato anche la creazione di quest’ultima classe per farne riferimento. Consapevoli di questo, andiamo a creare la prima classe JwtProvider, ovvero, la classe di utility responsabile della creazione e verifica del token JWT.

/**
 * The type Jwt provider.
 */
public class JwtProvider {

    private static final Logger log = LoggerFactory.getLogger(JwtProvider.class);
    private static final String issuer = "demo-service";

    /**
     * Create jwt string.
     *
     * @param subject       the subject
     * @param payloadClaims the payload claims
     * @return the JWT string
     */
    public static String createJwt(String subject, Map payloadClaims) {
        JWTCreator.Builder builder =  JWT.create()
                .withSubject(subject)
                .withIssuer(issuer)
                .withIssuedAt(DateTime.now().toDate())
                .withExpiresAt(DateTime.now().plusMonths(1).toDate());

        if (payloadClaims != null && !payloadClaims.isEmpty()) {
            for (Map.Entry entry : payloadClaims.entrySet()) {
                builder.withClaim(entry.getKey(), entry.getValue().toString());
            }
        } else {
            log.warn("You are building a JWT without header claims!");
        }
        return builder.sign(Algorithm.HMAC256(SecurityConfig.secret));
    }

    /**
     * Verify jwt decoded.
     *
     * @param jwt the JWT string
     * @return the decoded JWT
     */
    public static DecodedJWT verifyJwt(String jwt) {
        return JWT.require(Algorithm.HMAC256(SecurityConfig.secret)).build().verify(jwt);
    }

    private JwtProvider() {}
}

La classe implementa due metodi. Il primo, createJwt, serve, come suggerisce il nome, per generare un nuovo token JWT, il secondo, verifyJwt, ne verifica la validità decodificandolo.

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 metodo createJwt appena visto riceve una mappa di claims come parametro, aggiungendo tutti gli elementi al token.

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

 

Il parametro SecurityConfig.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 http esterna.

La nuova classe AuthorizationFilter deve estendere BasicAuthenticationFilter e implementare il metodo ereditato doFilterInternal. Una sua implementazione è la seguente:

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

    /**
     * Instantiates a new Authorization filter.
     *
     * @param authenticationManager the authentication manager
     */
    @Autowired
    public AuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader(SecurityConfig.param);
        if (header != null && header.startsWith(SecurityConfig.prefix)) {
            DecodedJWT decoded = JwtProvider.verifyJwt(header.replace(SecurityConfig.prefix, ""));
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(decoded.getSubject(), null, Collections.emptyList())
            );
        }
        chain.doFilter(request, response);
    }
}

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 finalmente una configurazione appropriata.

 

La classe SecurityConfig (configurazione)

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

    public static String secret;
    public static String param;
    public static String prefix;

    @Autowired
    public SecurityConfig(Environment env) {
        SecurityConfig.secret = env.getProperty("security.secret");
        SecurityConfig.param = env.getProperty("security.param");
        SecurityConfig.prefix = env.getProperty("security.prefix");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().addFilter(new AuthorizationFilter(authenticationManager())).authorizeRequests()
                .antMatchers("/public/**", "/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**").permitAll()
                .anyRequest().authenticated();
    }
}

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

Cerchiamo di capire meglio quanto scritto.

I primi tre attributi che vedi, sono proprio i campi definiti in application.properties e utilizzati dalle classi precedenti. Questi, sono campi statici e vengono valorizzati all’interno del costruttore all’avvio dell’applicazione leggendo appunto il valore dalle proprietà.

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 ApplicationUserMapper mapper;
    
    @Autowired 
    private UserService userService; 
    
    @PostMapping 
    public ResponseEntity<LoginOutputDto> signin(@RequestBody LoginInputDto body) { 
        // verifica se l'utente è registrato su db
        ApplicationUser user = userService.getByUsernameAndPassword(body.getUsername(), body.getPassword());
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        Map claimMap = new HashMap<>();
        claimMap.put("user", new ObjectMapper().valueToTree(this.mapper.toDto(user)));

        String jwt = JwtProvider.createJwt(user.getUsername(), claimMap);
        LoginOutputDto dto = new LoginOutputDto();
        dto.setToken(jwt);
        return ResponseEntity.ok(dto); 
    } 
}

All’interno di signin dovrebbe essere inserito un controllo sull’utente (è presente nel database?). Se è presente, viene restituito l’oggetto LoginOutputDto contenente il token JWT generato e valido per le successive chiamate.

 

ApplicationUserMapper è il componente che implementa la conversione da entità (ApplicationUser) a DTO (ApplicationUserDto), ad esempio:

/**
 * The type Application user mapper.
 */
@Component
public class ApplicationUserMapper {

    /**
     * To dto application user dto.
     *
     * @param entity the entity
     * @return the application user dto
     */
    public ApplicationUserDto toDto(ApplicationUser entity) {
        ApplicationUserDto dto = new ApplicationUserDto();
        dto.setAge(entity.getAge());
        dto.setEmail(entity.getEmail());
        dto.setId(entity.getId());
        dto.setName(entity.getName());
        dto.setUsername(entity.getUsername());
        return dto;
    }
}

ApplicationUserDto è una qualsiasi classe DTO che contiene una versione ridotta di attributi rispetto alla classe entity, affinché venga trasferito il contenuto all’esterno dell’applicazione.

 

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. Trattandosi di una API di accesso senza autenticazione, deve essere esclusa dalla catena di filtri Spring Security, ad esempio, impostando la rotta sotto la path public/… .

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:

 

Link utili:

 

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

Recommended Posts

12 Comments

  1. Ciao ancora,
    ho un warning nella classe LoginOutputDto. Precisamente mi suggerisce di aggiungere serial version Id, ma nel tuo codice non è presente nessun seriale…

    • Puoi aggiungerlo all’interno della classe come primo parametro …
      private static final long serialVersionUID = 1L;

  2. Ciao Alessandro, un’altra domanda:
    nel JwtProvider vedo che hai una annotazione @Slf4j che è di lombok. Quindi bisogna utilizzare lombok per questo progetto? Non trovo il punto dove dici di inserire anche questo nelle dependency.
    Grazie

    • Assolutamente no. Nella classe specifica JwtProvider viene usato solo per il log dell’errore (log.error(“Invalid JWT”, e);) per comodità 😉

  3. Ciao Alessandro, grazie del tuo articolo. Avrei una domanda.
    Nel mio application.properties inserendo il tuo suggerimento riscontro dei problemi.
    Mi accorgo che tu utilizzi un file yml.
    Ma il problema credo che non sia questo. In effetti param, prefix e secret sembrano non esistere come proprietà si security…
    Con cosa le posso sostituire?

    security:
    secret: chiavesupersegretissima
    prefix: ‘Bearer ‘
    param: Authorization

    • Ciao, sono proprietà custom per cui puoi sostituirle come vuoi, basta poi utilizzarle correttamente (@Value(“${security.prefix}” ecc..).
      Cosa importante, rispetta la sintassi e la formattazione corretta in base all’estensione che usi (.properties o .yaml). Se usi application.properties la forma è quella indicata in questo articolo.
      Se hai dubbi puoi leggere anche questo articolo.

  4. 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.

  5. 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.

  6. 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 *