In un articolo precedente, ho spiegato come implementare una classe RestController per la realizzazione di API REST in Spring Boot senza però alcun riferimento alla loro sicurezza.
In questo articolo, ti mostrerò un’implementazione dello standard OAuth2 per mettere in sicurezza le API di un’applicazione Spring Boot utilizzando Spring Security.
Prima di procedere, occorre definire alcuni dettagli per l’implementazione, in particolare, utilizzeremo:
- un access token di tipo Bearer Token
- una rappresentazione del Bearer Token in formato JWT
Introduzione a OAuth2
Spring Security, OAuth2, Bearer Token, JWT… ma di che stiamo parlando?!
Facciamo chiarezza.
OAuth2 è un protocollo standard utilizzato in ambiente web per l’autorizzazione delle richieste di accesso alle risorse, ad esempio, nella comunicazione tra Client e Server. In un contesto simile, un Client richiede una risorsa ad un Web Service inviando nell’header della richiesta un access token autorizzato.
Bearer Token è un particolare tipo di access token utilizzato per autorizzare le richieste in contesto OAuth2, mentre JWT (JSON Web Token) è un formato di rappresentazione dell’access token contenente informazioni di autenticazione.
In particolare, un token JWT ha una rappresentazione JSON delle informazioni. La sua struttura è composta da un header, un payload (un set di informazioni chiamati Claims personalizzabile) e una signature. Un access token di tipo JWT, infatti, può essere firmato tramite un algoritmo di cifratura, ad esempio SHA256.
Sicurezza delle API REST in Spring Boot
Una volta compreso quanto detto nel paragrafo precedente, non resta che racchiudere tutte queste informazioni in ambiente Spring Boot.
In pratica, dovrai realizzare un’applicazione Spring Boot che funge da Authorization Server con le seguenti funzionalità:
- autenticazione: espone una API di autenticazione dell’utente con generazione del token JWT
- autorizzazione: autorizza ogni richiesta validando il JWT presente nell’header
Cosa vedrai continuando la lettura
Vedrai come realizzare un’API di login per autenticare l’utente generando un token JWT codificato con chiave segreta e contenente alcune informazioni.
Poiché ogni richiesta conterrà l’access token, vedrai come validare il JWT ricevuto per autorizzare la richiesta.
Pronto? Iniziamo.
Utilizzare Spring Security con JWT per la sicurezza delle API in Spring Boot
Perché Spring Security?
Spring Security è un framework che fornisce autenticazione, autorizzazione e altre funzionalità di sicurezza delle applicazioni Java ed è, di fatto, lo standard per la sicurezza di applicazioni basate su Spring.
Inoltre, Spring Security è altamente personalizzabile e adatto al contesto di questo articolo.
Utilizzerai Spring Security per l’implementazione della sicurezza delle API in Spring Boot seguendo i passaggi:
#1 Aggiungere le dipendenze
Il primo passo consiste nell’aggiungere Spring Security al progetto utilizzando la dipendenza Maven o Gradle a seconda del gestore utilizzato. In questo esempio utilizzeremo Maven.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Aggiungi anche le dipendenze java-jwt e joda-time, serviranno per la realizzazione del JWT.
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.13</version>
</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
Una volta aggiunte le dipendenze al progetto, crea le classi Java:
- JwtProvider per la gestione del token JWT
- AuthorizationFilter per l’autorizzazione delle chiamate HTTP in ingresso
- SecurityConfig per tutte le altre configurazioni richieste da Spring Security
Parametrizzare i dati di configurazione
Alcune proprietà comuni possono essere parametrizzate all’interno del file application.properties, ovvero security.secret, security.prefix e security.param. Rappresentano rispettivamente la chiave segreta per la codifica del token JWT, il prefisso della stringa di autorizzazione e il nome del parametro presente nell’header della richiesta. Non preoccuparti, 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à)
Crea una classe JwtProvider che rappresenta la classe di utilità per la gestione del token JWT.
@Component
public class JwtProvider {
private static final Logger log = LogManager.getLogger(JwtProvider.class);
public static final String issuer = "demo-api-app";
public static String secret;
public static String prefix;
public static String headerParam;
@Autowired
public JwtProvider(Environment env) {
JwtProvider.secret = env.getProperty("security.secret");
JwtProvider.prefix = env.getProperty("security.prefix");
JwtProvider.headerParam = env.getProperty("security.param");
if (JwtProvider.secret == null || JwtProvider.prefix == null || JwtProvider.headerParam == null) {
throw new BeanInitializationException("Cannot assign security properties. Check application.yml file.");
}
}
public static String createJwt(String subject, Map<String, Object> payloadClaims) {
JWTCreator.Builder builder = JWT.create().withSubject(subject).withIssuer(issuer);
final DateTime now = DateTime.now();
builder.withIssuedAt(now.toDate()).withExpiresAt(now.plusDays(1).toDate());
if (payloadClaims.isEmpty()) {
log.warn("You are building a JWT without header claims");
}
for (Map.Entry<String, Object> entry : payloadClaims.entrySet()) {
builder.withClaim(entry.getKey(), entry.getValue().toString());
}
return builder.sign(Algorithm.HMAC256(JwtProvider.secret));
}
public static DecodedJWT verifyJwt(String jwt) {
return JWT.require(Algorithm.HMAC256(JwtProvider.secret)).build().verify(jwt);
}
}
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, mentre viene fatto uso dell’oggetto DateTime fornito dalla libreria joda-time per la gestione della scadenza del token stesso.
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 valorizzata dalla proprietà security.secret inserita nel file application.properties.
Le proprietà security.prefix e security.param, lette anch’esse dal file application.properties, rappresentano rispettivamente il prefisso della stringa che contiene il token JWT e il nome del parametro contenente questo valore all’interno dell’header della richiesta. Questa è una caratteristica del meccanismo Oauth2. Ad esempio, nell’header della richiesta possiamo trovare un’autenticazione di tipo Bearer: Bearer token.
La classe AuthorizationFilter (filtro)
Ora, crea una classe AuthorizationFilter che 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:
public class AuthorizationFilter extends BasicAuthenticationFilter {
private final UserRepository userRepository;
private final ObjectMapper mapper;
public AuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
this.mapper = new ObjectMapper();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
final String header = request.getHeader(JwtProvider.headerParam);
if (header != null && header.startsWith(JwtProvider.prefix)) {
final DecodedJWT decoded = JwtProvider.verifyJwt(header.replace(JwtProvider.prefix, ""));
final ObjectNode userNode = this.mapper.readValue(decoded.getClaim("user").asString(), ObjectNode.class);
final User user = this.mapper.convertValue(userNode, User.class);
this.userRepository.findById(user.getId()).ifPresent(entity -> {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null, entity.getRoles().stream().map(UserRole::name).map(SimpleGrantedAuthority::new).collect(Collectors.toSet())));
});
}
chain.doFilter(request, response);
}
}
Esaminiamo ora 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 questo metodo dovrà fare è verificare il token presente nella richiesta e decidere se autorizzare la richiesta inserendo l’autenticazione nella catena dei filtri.
Per prima cosa si estrae dalla richiesta e si verifica il parametro header “Authorization”. Nel caso di esito positivo proseguiremo con la decodifica del token, viceversa, verrà restituito al client un errore di autenticazione. Se anche la decodifica ha esito positivo, ovvero la chiave segreta utilizzata per la firma ha corrispondenza, 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.
A questo punto dovrai aggiungere il filtro appena creato alla configurazione di Spring Security, come vedrai nella prossima classe.
La classe SecurityConfig (configurazione)
Per implementare Spring Security con JWT devi 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.
Per la nostra implementazione con JWT vogliamo:
- escludere i cookie
- non creare delle sessioni (la creiamo noi)
- modificare il comportamento della filter chain per validare il token
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private UserRepository userRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new AuthorizationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)), this.userRepository), UsernamePasswordAuthenticationFilter.class).authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
return http.build();
}
}
La classe SecurityConfig qui sopra rappresenta la nostra configurazione per Spring Security.
Cerchiamo di capire meglio quanto scritto.
SecurityConfig viene annotata con @Configuration affinché venga trattata come tale da 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.
Dalla versione 5.7.0-M2 di Spring Security, che trovi in Spring Boot 2.7.x, la classe WebSecurityConfigurerAdapter è stata deprecata. Scopri qui come effettuare l'aggiornamento.
Occorre definire il Bean filterChain, nell’esempio:
http.cors().and().csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
rispettivamente per disabilitare CORS e CSRF e utilizzare una politica stateless delle sessioni.
.addFilterBefore(new AuthorizationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)), this.userRepository), UsernamePasswordAuthenticationFilter.class).authorizeRequests()
per aggiungere una nuova istanza del filtro AuthorizationFilter creato all’interno della filter chain (catena dei filtri) di Spring Security, subito prima del filtro UsernamePasswordAuthenticationFilter.
.antMatchers("/public/**").permitAll()
per autorizzare sempre le API che iniziano con “/public”. Riguarda le API che vogliamo esporre pubblicamente, e che quindi non devono essere autenticate, come il login.
.anyRequest().authenticated();
per autenticare tutte le altre richieste.
Cosi facendo, abbiamo configurato correttamente Spring Security per gestire un’autenticazione con JWT.
#3 Definire una API per l'autenticazione
Di seguito, un esempio di API di autenticazione (login) per la generazione del JWT da parte del server.
public class AuthenticationDto {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@RestController
@RequestMapping("public/authentication")
public class AuthenticationController {
@Autowired
private AuthenticationService authenticationService;
@PostMapping
public String login(@RequestBody AuthenticationDto authDto) {
return this.authenticationService.login(authDto.getEmail(), authDto.getPassword());
}
}
@Service
public class AuthenticationService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public String login(String email, String password) {
User user = this.userRepository.findByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!this.passwordEncoder.matches(password, user.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials");
}
ObjectNode userNode = new ObjectMapper().convertValue(user, ObjectNode.class);
userNode.remove("password");
Map claimMap = new HashMap<>(0);
claimMap.put("user", userNode);
return JwtProvider.createJwt(email, claimMap);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
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:
- Visualizza il codice di esempio su GitHub: demo-api-service
- Connetti l’applicazione ad un database e documenta le tue API
- Approfondisci leggendo altri articoli del sito
Link utili:
26 Comments
A me da problemi nella classe AuthorizationFilter
SecurityContextHolder.getContext().setAuthentication((Authentication) new UsernamePasswordAuthenticationToken(user, null, entity.getRoles().stream().map(UserRole::name).map(SimpleGrantedAuthority::new).collect(Collectors.toSet())));
“The type UserRole does not define name(User) that is applicable here”
Ciao,
dopo che ho effettuato il login (200) e preso il token, se provo con altre mie api che richiedono l’autenticazione mi da sempre 403.
Su postman ho aggiunto in authorization il token selezionando la voce bearer token.
Devo aggiungere qualcosa nei miei controller sulle varie api per fargli permettere l’autenticazione?
Grazie
Ciao,
ho un problema con le altre api:
Dopo che ho effettuato il login (200) e preso il token se inserisco in postman in autenticazione con bearer token il token appena creato e faccio partire la request mi da 403.
Nei controller che richiedono l’autenticazione devo aggiungere qualcosa?
Non riesco proprio a capire cosa sbaglio.
Ciao, ma per provare da Insomnia/Postman, oltre a mandare il body json con username e password, devo settare anche il Token Bearer? se si, come lo genero?
Ciao, se intendi la chiamata di login (/public/authentication) non serve passare il token; è proprio questa che lo genera.
Questo vale se hai configurato correttamente il tutto, ovvero, hai aggiunto “.antMatchers(“/public/**”).permitAll()” in SecurityConfig e definito l’endpoint “public/authentication” in AuthenticationController.
si ora funziona, grazie
Mentre per il discorso del refresh del token, leggevo sotto che devo creare un token con scadenza più lunga; lo posso poi memorizzare nel db normalmente senza cifrarlo a sua volta?
Ciao sto utilizzando postman faccio il login e mi restituisce il jwt composto da tre parti divise dal punto.
Ho implementato un nuovo semplice controller con una get di test..
Ho provato a fare la get mettendo in header chiave: Authorization e valore: Bearer “iltoken” ma mi restituisce sempre 403 ho provato in tutti i modi ma niente anche dalla scheda authorization di postaman impostando bearer e passando il token ma niente.
EDIT:
Passando in modo esatto il token cioè: ‘Bearer ‘ token
mi restituisce l’errore JWTDecodeException: The input is not a valid base 64 encoded string.
Ciao, probabilmente non passi nel modo corretto il parametro header, i.e. ‘Authorization=Bearer token’.
In Postman, apri il tab Authorization e seleziona il tipo ‘Bearer token’, poi nella input ‘Token’ incolla il tuo token jwt senza la scritta Bearer (già impostato come tipo).
Ciao e complimenti per la guida, veramente ben fatta e soprattutto ben commentata! Hai qualche suggerimento su come poter implementare anche il meccanismo di refresh token nel caso in cui il token fosse scaduto, in modo da poterlo rinnovare senza rifare il login da parte dell’utente?
Grazie ancora per il tuo lavoro!
Ciao, grazie per il commento.
L’utilizzo del refresh token serve a posticipare la scadenza del token scambiato in tutte le richieste (access token) prima che questo scada. Quest’ultimo ha solitamente una scadenza a breve termine proprio per la sua frequenza di utilizzo.
Per l’implementazione di un meccanismo di refresh, l’idea è quella di generare in fase di login un refreshToken con scadenza a lungo termine oltre all’accessToken. Realizzare quindi un’API di refresh che riceve il refreshToken e ritorna un accessToken aggiornato, ovvero, con scadenza a breve termine posticipata. Alla scadenza del refreshToken si effettua nuovamente il login.
Il client dovrà quindi gestire la chiamata di refresh, solitamente quando si avvicina la scadenza dell’accessToken, oltre alla la memorizzazione locale di entrambi i token.
Spero di aver risposto alla tua domanda.
Ben fatto, peccato che venga utilizzato il WebSecurityConfigurerAdapter attualmente deprecato.
Spero in futuro di vederlo aggiornato.
Complimenti
Ciao, grazie per il commento. Ho modificato l’articolo per sostituire la classe deprecata ;).
In questo post ho scritto come effettuare l’aggiornamento del codice.
Ciao
Davvero un ottima guida!
Molto chiara e di sicuro meglio di molti altri modi di autenticazione, però è un peccato che una cosa del genere non sia stata implementata in automatico, magari tramite l’utilizzo di uno starter di spring-boot, sarebbe molto comodo poter usare un sistema di autenticazione del genere senza dover riscrivere tutto questo ogni volta
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;
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à 😉
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.
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.
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.
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.