< Home
Stampa

Web Service: Cache Remota

Sommario

In questa lezione vedremo l’implementazione della cache remota. Un progetto simile è presente su questo sito alla url https://ws.cipiaceinfo.it/register (scritto in NodeJs).

L’obiettivo della lezione è creare una applicazione più strutturata di quella vista nella precedente lezione:

  • saranno previsti più controller e servizi;
  • verrà introdotta l’autenticazione;
  • verranno introdotte nuove classi e moduli.

Descrizione

Questo Web service prevede i seguenti servizi

  • POST /api/auth/login : servizio che consente all’utente di ottenere un token di autenticazione per utente registrato
  • POST /api/auth/register : servizio che consente di registrare un nuovo utente
  • POST /api/data/set : servizio che permette di registrare una nuova coppia chiave-valore
  • GET /api/data/get/{key} : servizio che permette di richiedere un valore data la chiave

Il formato dati sarà:

{
 "key": "...",
 "value": "..."
}

In pratica:

  • un utente si registra
  • effettua login tramite credenziali con cui si è registrato e riceve un token
  • con il token può salvare coppie chiave-valore per futuri utilizzi
  • può accedere al valore di ciascuna chiave con la chiave (ed il token)

In pratica quello che andremo a creare è un semplice database di tipo Key-Value store, persistente, accessibile da ovunque con applicazioni client ad esempio in Javascript.

Per l’autenticazione useremo i token JWT, spiegati nella lezione sulle REST API.

Model

Il model consiste in una unica entità

package it.cipiaceinfo.remotecache.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "users")
public class User {
    
    @Id @GeneratedValue
    private Long id;

    @Column(unique=true, nullable=false)
    private String username;

    @Column(nullable=false)
    private String password;

    @Column(columnDefinition="CLOB")
    private String keyValuesJSON; 

    public User() {}

    public User(LoginDTO loginDTO) {
        this.username = loginDTO.username();
        this.password = loginDTO.password();
        this.keyValuesJSON = "{}";
    }

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }

    public String getKeyValueJSON() {
        return this.keyValuesJSON;
    }

    public void setKeyValueJSON(String keyValueJSON) {
        this.keyValuesJSON = keyValueJSON;
    }

}

Usiamo un insieme di annotazioni:

  • @Table: serve per indicare il nome della tabella nel database
  • @Column: consente di definire alcune caratteristiche della colonna, come unique=true (non possono esserci due valori uguali) e nullable=false (deve essere sempre valorizzato) e columnDefinition (“CLOB” indica un tipo di dato per memorizzare grandi quantità di testo)

Le coppie chiave valore saranno memorizzate direttamente in formato JSON.

[{
 "key": "...",
 "value": "..."
},
{
 "key": "...",
 "value": "..."
},
..
]

Qui il repository:

package it.cipiaceinfo.remotecache.model;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long>{
    Optional<User> findByUsername(String username);
}

Come si può notare il metodo findByUsername non è implementato.
JPA/Hibernate intercetta la chiamata alla funzione “findBy”+[nomecampo] e genera automaticamente la query al database senza che il programmatore scriva il codice.

Completiamo il model coi DTO necessari (sono in file differenti, sono accorpati qui per semplicità):

package it.cipiaceinfo.remotecache.model;

public record LoginDTO(String username, String password) {
    
}

public record KeyValueDTO(String key, String value) {
    
}

public enum Response {
    FOUND,
    NOTFOUND,
    UNAUTHORIZED
}


public record LoginResponseDTO(Response response, String username) {
    
}


public record TokenDTO(String token) {
    
}

Il record è un tipo di dato Java (versione 16 e successive) che serve per gestire i DTO. In pratica Java in fase di compilazione genera attributi e getter/setter automaticamente.

  • LoginDTO: rappresenta le informazioni relative alla login/registrazione utente
  • KeyValueDTO: rappresenta la singola coppia chiave-valore
  • LoginResponseDTO: indica l’esito della login utente (con il relativo enum)
  • TokenDTO: incapsula il token di autenticazione utente

Service

L’applicazione prevede due servizi:

  • AuthService: il servizio che gestisce autenticazione e login
  • CacheService: il servizio che gestisce la set e la get delle chiavi

AuthService

package it.cipiaceinfo.remotecache.services;

import java.util.Optional;

import org.springframework.stereotype.Service;

import it.cipiaceinfo.remotecache.model.LoginDTO;
import it.cipiaceinfo.remotecache.model.LoginResponseDTO;
import it.cipiaceinfo.remotecache.model.Response;
import it.cipiaceinfo.remotecache.model.User;
import it.cipiaceinfo.remotecache.model.UserRepository;

@Service
public class AuthService {
    private final UserRepository userRepository;

    public AuthService(UserRepository userRepository) {
        this.userRepository = userRepository;        
    }

    public LoginResponseDTO login(LoginDTO loginDTO) {
        Optional<User> optionalUser = userRepository.findByUsername(loginDTO.username());
        if (optionalUser.isPresent()) {
            User user = optionalUser.get();
            if (loginDTO.password().equals(user.getPassword())) {
                return new LoginResponseDTO(Response.FOUND, user.getUsername());
            } else {
                return new LoginResponseDTO(Response.UNAUTHORIZED, null);                
            }
        } else {
            return new LoginResponseDTO(Response.NOTFOUND, null);
        }
    }

    public boolean register(LoginDTO loginDTO) {
        Optional<User> optionalUser = userRepository.findByUsername(loginDTO.username());
        if (optionalUser.isPresent()) {
            return false;
        } else {
            User user = new User(loginDTO);
            userRepository.save(user);
            return true;
        }
    }

    public Optional<User> getUser(String username) {
        return userRepository.findByUsername(username);
    }
}

I due metodi principali sono login e register, più un terzo metodo che serve per cercare l’utente (servirà per l’autenticazione col token).

Il CacheService:

package it.cipiaceinfo.remotecache.services;

import java.util.Map;

import org.springframework.stereotype.Service;

import it.cipiaceinfo.remotecache.model.User;
import it.cipiaceinfo.remotecache.model.UserRepository;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;

@Service
public class CacheService {
    private static final ObjectMapper mapper = new ObjectMapper();
    private UserRepository userRepository;    

    public CacheService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void set(User user, String key, String value) {
        Map<String, String> keyValues = mapper.readValue(
            user.getKeyValueJSON(), 
            new TypeReference<Map<String, String>>() {}
        keyValues.put(key, value);
        user.setKeyValueJSON(mapper.writeValueAsString(keyValues));
        userRepository.save(user);
    }

    public String get(User user, String key) {
        Map<String, String> keyValues = mapper.readValue(
            user.getKeyValueJSON(), 
            new TypeReference<Map<String, String>>() {}
        return keyValues.get(key);
    }
}

La classe ObjectMapper viene dalla libreria Jackson, una libreria molto utilizzata per convertire da e verso JSON in modo automatico. Il metodo readValue(json, typereference) legge il JSON. TypeReference è un contenitore generico che serve per indicare all’Object Mapper che la classe che deve creare è una Map<String, String>.

Sicurezza

Come detto sopra, l’accesso alle chiavi è ammesso solo da utenti registrati che hanno effettuato login con un token valido. Il token utilizza lo standard JWT, che consente di criptare un insieme di informazioni da inviare al client:

  • username
  • data di creazione
  • data di scadenza

Non è necessario salvare a database i token, la sicurezza è garantita dal fatto che vengono criptati/decriptati lato server da una chiave privata.

Scriviamo quindi una classe di utilità che gestisce criptazione e decriptazione del token:

package it.cipiaceinfo.remotecache.security;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

import org.springframework.stereotype.Component;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

@Component
public class JwtUtil {
    private final String SECRET = "mysecretKey";

    public String generateToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
            .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
            .compact();
    }

    public String extractUsername(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(SECRET.getBytes())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}

Come si vede JwtUtil usa la libreria Jwts, che estrae lo username dal token (il Subject). Se il token non è valido restituisce un oggetto vuoto.

La sicurezza è implementata tramite un filtro. In pratica è possibile configurare il Web Service in modo tale che ogni richiesta che arriva dal Web Server sia prima processata da un filtro e se ritenuta valida, passata al Controller. Più precisamente è possibile configurare una catena di filtri che in sequenza elaborano la richiesta, non necessariamente per bloccarla, ma anche per salvare informazioni, per esempio provenienti dall’Header (ad esempio cookies, statistiche, ecc.).

Quella che va configurata quindi è una FilterChain, che in questo specifico WebService prevede un solo filtro, quello di autenticazione. Serve quindi:

  • una classe filtro che implementa una specifica interfaccia di filtro
  • una classe configurazione che inserisce il fitro nella FilterChain

Creiamo quindi JwtFilter

package it.cipiaceinfo.remotecache.security;

import java.util.List;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import it.cipiaceinfo.remotecache.services.AuthService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final AuthService authService;

    public JwtFilter(JwtUtil jwtUtil, AuthService authService) {
        this.jwtUtil = jwtUtil;
        this.authService = authService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain) throws ServletException, java.io.IOException {
        System.out.println("In esecuzione filter.");
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.replace("Bearer ", "");
            try {
                String username = jwtUtil.extractUsername(token);
                authService.getUser(username)
                        .ifPresent(user -> {                            
                            Authentication auth
                                    = new UsernamePasswordAuthenticationToken(
                                            user, null, List.of());
                            var context = SecurityContextHolder.createEmptyContext();
                            context.setAuthentication(auth);
                            SecurityContextHolder.setContext(context);
                        });
            } catch (Exception ignored) {
                System.out.println("Token non valido");
                // token non valido → richiesta non autenticata
            }
        }
        filterChain.doFilter(request, response);
    }
}

Note:

  • l’interfaccia è OncePerRequestFilter: impone che il fitro sia eseguito una sola volta per richiesta (ovvero che nella FilterChain sia eseguito solo una volta);
  • come si vede, il fitro riceve la richiesta HTTP (HttpServletRequest) la risposta Http (HttpServletResponse) ed una istanza di FilterChain
  • il filtro estrae il token dall’header
  • a questo punto estrae lo username dal token (usando JwtUtil).
  • lo verifica chiedendo ad AuthService
  • se esiste allora crea una istanza della classe Authentication
  • crea quindi un oggetto SecurityContextHolder e gli passa l’istanza di autenticazione. Questo oggetto è un oggetto che Spring crea nella attuale sessione di richiesta, e sarà utilizzato poi dal Controller per verificare che esiste una autenticazione valida.
  • infine passa al filtro successivo (va messo per ragioni di compatibilità ed estensibilità)

Ora possiamo inserire il fitro in configurazione, con la classe SecurityConfig:

package it.cipiaceinfo.remotecache.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    private final JwtFilter jwtFilter;

    public SecurityConfig(JwtFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**")
                .permitAll()
                .anyRequest()
                .authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

Vediamo in dettaglio:

  • @Configuration: questa annotazione dice a Spring che questa classe estende la configurazione dell’applicazione
  • @EnableMethodSecurity: dice a Spring che questa applicazione deve attivare la FilterChain per i controllers
  • il metodo securityFilterChain riceve come parametro l’istanza di httpSecurity (la classe che gestisce la sicurezza delle chiamate) e poi:
    • disabilita CSRF (https://it.wikipedia.org/wiki/Cross-site_request_forgery), in questo contesto non serve perché il token è passato nell’header, non è quindi possibile eseguire la richiesta senza possedere ed inviare esplicitamente il token, che non può essere conosciuto dall’hacker;
    • autorizza le richieste alle api di autenticazione (che sono senza token)
    • abilita il fitro JWT per le altre API (quelle sui dati)
  • in effetti non esiste un meccanismo di sicurezza per l’autenticazione (chiunque si può autenticare). Un meccanismo più sicuro può essere quello di aggiungere la sicurezza CORS (https://it.wikipedia.org/wiki/Cross-origin_resource_sharing)

Si tratta comunque di un progetto didattico, quindi la sicurezza introdotta è quella minima necessaria per capire come funziona.

Controllers

Implementiamo quindi i due controllers, uno per l’autenticazione, l’altro per i dati

package it.cipiaceinfo.remotecache.controllers;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import it.cipiaceinfo.remotecache.model.LoginDTO;
import it.cipiaceinfo.remotecache.model.TokenDTO;
import it.cipiaceinfo.remotecache.security.JwtUtil;
import it.cipiaceinfo.remotecache.services.AuthService;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import it.cipiaceinfo.remotecache.model.LoginResponseDTO;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthService authService;
    private final JwtUtil jwtUtil;

    public AuthController(AuthService authService, JwtUtil jwtUtil) {
        this.authService = authService;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/login")
    public ResponseEntity<TokenDTO> login(@RequestBody LoginDTO loginDTO) {
        System.out.println("Richiamato servizio LOGIN.");
        LoginResponseDTO responseDTO = authService.login(loginDTO);
        switch (responseDTO.response()) {
            case FOUND:
                System.out.println("Login utente: " + loginDTO.username());
                return ResponseEntity
                        .status(HttpStatus.OK)
                        .body(new TokenDTO(
                            jwtUtil.generateToken(
                                responseDTO.username())
                        ));
            case UNAUTHORIZED:
                System.out.println("Login errata utente: " + loginDTO.username());
                return ResponseEntity
                        .status(HttpStatus.UNAUTHORIZED)
                        .body(null);
            default:
                System.out.println("Utente non trovato: " + loginDTO.username());
                return ResponseEntity
                        .status(HttpStatus.NOT_FOUND)
                        .body(null);
        }
    }

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody LoginDTO loginDTO) {
        System.out.println("Richiamato servizio REGISTER.");
        if (!authService.register(loginDTO)) {
            System.out.println("Registrato utente: " + loginDTO.username());
            return ResponseEntity
                .status(HttpStatus.NOT_ACCEPTABLE)
                .body("User already exists");
        } else {
            System.out.println("Utente esistente: " + loginDTO.username());
            return ResponseEntity
                .status(HttpStatus.OK)
                .body("User Registered");
        }
    
    }

}

Qui introduciamo la classe ResponseEntity<T>, un contenitore che rappresenta la HTTP Response, con i suoi metodi status, header e body. La WebApplication la tradurrà in un oggetto HttpServletResponse che sarà inviata al Web Server.

CacheController

package it.cipiaceinfo.remotecache.controllers;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import it.cipiaceinfo.remotecache.model.KeyValueDTO;
import it.cipiaceinfo.remotecache.model.User;
import it.cipiaceinfo.remotecache.services.CacheService;

@RestController
@RequestMapping("/api/data")
public class CacheController {
    private final CacheService cacheService;    
    
    public CacheController(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    @PostMapping("/set")
    public ResponseEntity<String> set(@RequestBody KeyValueDTO keyValueDTO, Authentication authentication) {
        System.out.println("Richiamato servizio SET per la chiave: " + keyValueDTO.key() + " valore: " + keyValueDTO.value());
        User user = (User)authentication.getPrincipal();
        cacheService.set(user, keyValueDTO.key(), keyValueDTO.value());
        return ResponseEntity
            .status(HttpStatus.OK)
            .body("Added.");
    }

    @GetMapping("/get/{key}")
    public ResponseEntity<KeyValueDTO> getMethodName(@PathVariable String key, Authentication authentication) {
        System.out.println("Richiamato servizio GET per la chiave: " + key);
        User user = (User)authentication.getPrincipal();                
        return ResponseEntity
            .status(HttpStatus.OK)
            .body(new KeyValueDTO(key, cacheService.get(user, key)));
    }    
}

Configurazione

Per rendere più chiaro il progetto usiamo questa struttura:

/it/cipiaceinfo/remotecache
  /model
  /services
  /controllers
  /security

Pom.xml dipendenze:

    <dependencies>
        <!-- REST -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <!-- JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- JWT -->
        <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>

        <!-- DB -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

e application.properties:

spring.application.name=remotecache
spring.datasource.url=jdbc:h2:file:./data/cache-db
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
server.port=6502

Esecuzione

Eseguiamo:

mvn clean
mvn compile
mvn spring-boot:run

Per testare:

> curl -X POST http://localhost:6502/api/auth/register -H "Content-Type: application/json" -d '{"username": "cipiaceinfo", "password": "password"}'

User Registered

> curl -X POST http://localhost:6502/api/auth/register -H "Content-Type: application/json" -d '{"username": "cipiaceinfo", "password": "password"}'

User already exists

> curl -X POST http://localhost:6502/api/auth/login -H "Content-Type: application/json" -d '{"username": "cipiaceinfo", "password": "password"}'

{"token":"eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJjaXBpYWNlaW5mbyIsImlhdCI6MTc2NzQzODgyMCwiZXhwIjoxNzY3NDQyNDIwfQ.8-YeCp0BJ11fSaHU6OGkMpO3nsMcMwP7bNmHtSbcxL0ZbnKp_x9rsP2zhsoswlUj"}

> curl -X POST http://localhost:6502/api/data/set -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJjaXBpYWNlaW5mbyIsImlhdCI6MTc2NzQzODgyMCwiZXhwIjoxNzY3NDQyNDIwfQ.8-YeCp0BJ11fSaHU6OGkMpO3nsMcMwP7bNmHtSbcxL0ZbnKp_x9rsP2zhsoswlUj" -d '{"key": "MyKey", "value": "My Value"}'

Added.

> curl http://localhost:6502/api/data/get/MyKey -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJjaXBpYWNlaW5mbyIsImlhdCI6MTc2NzQzODgyMCwiZXhwIjoxNzY3NDQyNDIwfQ.8-YeCp0BJ11fSaHU6OGkMpO3nsMcMwP7bNmHtSbcxL0ZbnKp_x9rsP2zhsoswlUj"

{"key":"MyKey","value":"My Value"}

Conclusioni

In questo progetto abbiamo introdotto nuove classi e nuovi strumenti:

  • Servizi e controller multipli
  • Separazione tra Entity e DTO
  • Livello di sicurezza, con JWT, FilterChain e Configuration

Con questi strumenti l’applicazione diventa più simile ad una applicazione reale e consente di comprendere meglio sia la Dependency Injection, sia soprattutto il flusso.

Una cosa che si può osservare, in particolar modo con classi come ObjectMapper per i JSON, o la gestione della sicurezza con la FilterChain, le HttpServletRequest e HttpServletResponse, ResponseEntity, ecc. una certa verbosità del linguaggio Java, che si riflette nelle librerie, che introduce complessità spesso non necessaria e non intuitiva. Si pensi poi al SecurityContextHolder, che crea un oggetto Authentication, che viene poi “magicamente” passato come paramero ai metodi dei Controllers. O all’ObjectMapper che richiede per forza di definire una classe TypeInference<Map<String, String>> perché da solo non è in grado di riconoscere un dizionario.

Questo è Java, un linguaggio ed una tecnologia che presta molta attenzione ai formalismi, anche se porta a diverse idiosincrasie. Questo è il motivo per cui Java non è apprezzato da molti informatici, perché sembra talvolta complicare inutilmente la programmazione.

Tuttavia questa filosofia ha (almeno parzialmente) un senso. Questi formalismi ridondanti sono nati per evitare il più possibile errori di programmazione a runtime, consentendo al compilatore di scovare più facilmente incoerenze. Lo sforzo che si apprezzerebbe in futuro è magari di vedere applicata più spesso la Convention over Configuration, come per esempio abbiamo visto nel repository, dove basta dichiarare un metodo “findByUsername” ed il sistema capisce da solo cosa deve fare.

Del resto però l’informatica è, volenti o nolenti, anche questo. Java è presente nella maggioranza delle applicazioni di grandi dimensioni (se non è Java, è C#), e la sua diffusione impone di imparare talvolta passaggi ostici insieme ai vantaggi che comunque da. Maggiore astrazione significa anche maggiori possibilità di personalizzare e realizzare componenti anche molto complessi con meno errori a runtime.

D’altro canto questo codice è fortemente riusabile: si può prendere questo progetto e farne la base per progetti più complessi riutilizzando tutte o quasi le classi di autenticazione, quindi una volta capito come funziona, si può riusare in molti contesti diversi.