
Partager:
Ingénieur frontend senior basé à Mumbai, en Inde. Je travaille sur le développement fullstack avec React et Spring boot pour la Fintech la plus appréciée d'Inde. Je suis également le fondateur de learnersbucket.com où j'écris des guides de préparation aux entretiens JavaScript.
Authentification par OTP (JWT) dans Spring Boot avec l'API Verify de Vonage
Temps de lecture : 7 minutes
Cet article a été mis à jour en mars 2025
Vue d'ensemble
L'expérience utilisateur (UX) sans faille est un facteur déterminant pour la croissance des produits. L'authentification fait partie intégrante d'une bonne UX, notamment dans les applications bancaires ou FinTech.
Imaginez que vous ayez à créer une page de connexion pour une banque où le client utilise un numéro de téléphone et un mot de passe à usage unique (OTP) pour s'authentifier. Éliminer la nécessité de se souvenir d'un mot de passe tout en offrant la sécurité améliorerait l'expérience de l'utilisateur.
Voyons comment créer une authentification JWT (JSON Web Token) basée sur l'OTP en Java. Nous utiliserons le cadre Spring Boot et l'API Verify de Vonage.
Pour en savoir plus sur JWT ici.
Compte API Vonage
Avant de commencer à utiliser l'API, nous aurons besoin d'un Account API Vonage.

Une fois que nous avons notre clé API et le secret APInous les utiliserons pour la fonction Verify API.
Mise en place
Nous allons créer une nouvelle application Spring et importer le SDK du serveur Java de Vonage pour pouvoir utiliser les API de Vonage dans notre application. Nous ajoutons le code ci-dessous à notre fichier build.gradle ou au fichier POM.
Gradle
Ajoutez ce qui suit au fichier build.gradle le texte suivant.
repositories {
mavenCentral()
}
dependencies {
implementation 'com.vonage:server-sdk:8.20.1'
} Maven
Ajoutez ce qui suit au fichier POM de notre projet.
<dependency>
<groupid>com.vonage</groupid>
<artifactid>server-sdk</artifactid>
<version>8.20.1</version>
</dependency> Aperçu de la mise en œuvre
Maintenant que nous avons terminé la configuration, nous pouvons nous plonger dans le développement. Cette authentification par OTP avec JWT peut être réalisée en trois étapes :
Création de JWT
Filtrer les demandes des utilisateurs
Accéder aux fonctionnalités de l'API
Création de JWT
Nous utiliserons un mécanisme d'autorisation basé sur un jeton dans lequel, une fois l'utilisateur authentifié avec succès, nous générerons un nouveau jeton avec une période d'expiration et le renverrons à l'utilisateur.
L'utilisateur devra transmettre ce jeton à chaque demande pour prouver son identité et accéder ensuite aux applications nécessitant une autorisation.
Filtrer les demandes des utilisateurs
Chaque fois que nous recevons un jeton dans la requête, nous devons le vérifier et indiquer à Spring Security que l'utilisateur est autorisé et peut accéder aux points de terminaison restreints. Nous aurons besoin d'un filtre pour chaque requête.
Accéder aux fonctionnalités de l'API
Nous utiliserons trois routes API différentes pour compléter l'authentification. Chacune d'entre elles a un objectif unique :
Obtenir le numéro de téléphone de l'utilisateur pour envoyer l'OTP
Verify the OTP and return the JWT to the user (Vérifier l'OTP et renvoyer le JWT à l'utilisateur)
Point final général pour les essais
Voyons comment chacun de ces éléments peut être mis en œuvre séparément.
Gestion du cryptage des JWT
JWT est une combinaison de deux méthodes de cryptage différentes créées à l'aide de JWS et JWE, qui peuvent être cryptées à l'aide d'une clé symétrique. SECRET_KEY avec une charge utile.
La charge utile peut contenir des données sensibles que nous pouvons utiliser pour valider l'utilisateur (par exemple, la date d'expiration, les coordonnées de l'utilisateur, etc.)
En utilisant la même SECRET_KEYnous pouvons décrypter le jeton pour obtenir la charge utile et l'utiliser en cas de besoin.
Pour gérer les JWT, nous utiliserons le paquetage io.jsonwebtoken::jjwt paquetage.
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
}Créer une classe JWTUtils sous le paquetage util et ajoutez le code suivant.
package com.example.vonage.auth.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.cglib.core.internal.Function;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JWTUtil {
private final String SECRET_KEY = "secret key";
public String extractIdentifier(String token) {
return extractClaim(token, Claims::getSubject);
}
public 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().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token){
return extractExpiration(token).before(new Date());
}
public String generateToken(String details) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, details);
}
private String createToken(Map<String, Object> claims, String subject) {
long time = System.currentTimeMillis();
long expiry = Duration.ofDays(10).toMillis();
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(time))
.setExpiration(new Date(time + expiry))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, String identifier) {
final String phoneNumber = extractIdentifier(token);
return phoneNumber.equals(identifier) && !isTokenExpired(token);
}
}Il contient toute la logique relative au JWT, qui peut être utilisée pour créer un nouveau jeton, valider le jeton et obtenir des identifiants à partir du jeton pour valider l'utilisateur, etc.
Nous remplaçons ici le SECRET_KEY par n'importe quel secret partagé et nous y accédons à partir des fichiers de propriétés de l'application plutôt que de le coder en dur.
De même, lors de la génération du jeton, nous transmettons le paramètre détails (numéro de téléphone) sous forme de chaîne de caractères, mais vous pouvez stocker n'importe quel objet et l'extraire.
La validation est extrêmement simple : nous stockons simplement le numéro de téléphone dans le jeton, qui sera utilisé à chaque fois pour l'autorisation.
Dans la méthode validateToken nous comparons les numéros de téléphone et vérifions si le jeton a expiré.
La date d'expiration du jeton est de dix jours Duration.ofDays(10).toMillis() à compter de la création du jeton.
Ajouter un filtre pour authentifier un utilisateur et définir le contexte
Chaque fois que nous recevons une requête nécessitant une autorisation, nous devons valider le jeton et définir le contexte dans lequel l'utilisateur est authentifié.
Nous allons ajouter un nouveau filtre qui étendra le filtre OncePerRequestFilter- comme son nom l'indique ; ce filtre sera exécuté une fois pour chaque demande.
Créer une nouvelle classe Java nommée JWTFilter dans le paquetage filters et ajoutez le code suivant.
Il contient la logique permettant d'extraire le jeton de la demande et de le valider. S'il est autorisé, il définit le contexte dans lequel l'utilisateur est authentifié.
package com.example.vonage.auth.filters;
import com.example.vonage.auth.utils.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JWTFilter extends OncePerRequestFilter {
@Autowired
JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String jwt = null, identifier = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer")) {
jwt = authorizationHeader.substring(7);
identifier = jwtUtil.extractIdentifier(jwt);
}
if (identifier != null && jwtUtil.validateToken(jwt, identifier) && SecurityContextHolder.getContext().getAuthentication() == null) {
AuthenticationFilter apiToken = new AuthenticationFilter("abc", "xyz", AuthorityUtils.createAuthorityList());
SecurityContextHolder.getContext().setAuthentication(apiToken);
}
filterChain.doFilter(request, response);
}
}Pour chaque requête, nous récupérons l'en-tête Authorization et nous en extrayons le jeton après le texte BearerC'est pourquoi nous obtenons la sous-chaîne après les sept premiers caractères (y compris un espace après le mot Bearer).
Une fois que nous avons le jeton, nous extrayons l identifiant à des fins de validation.
Pour la validation, nous testons si l'utilisateur est déjà authentifié ou non, ce qui ne sera pas le cas puisque nous disposons de la fonction SANS ÉTAT et si l'identifiant est le même et si le jeton n'a pas expiré. La session est sans état parce que nous ne maintenons aucune session étant donné qu'il s'agit d'une autorisation basée sur un jeton. Ce paramètre sera mis à jour dans la sécurité de printemps.
Parce que nous n'avons utilisé que le numéro de téléphone comme identifiant, il n'y a pas de vérification croisée. Nous pouvons renforcer la sécurité en stockant l'adresse électronique de l'utilisateur ou tout autre objet unique, puis en utilisant le numéro de téléphone pour l'extraire de la base de données et procéder à une vérification.
Pour obtenir le contexte d'authentification, nous avons étendu la fonction AbstractAuthenticationToken et avons fourni une clé et un secret uniques.
AuthenticationFilter apiToken = new AuthenticationFilter("abc", "xyz", AuthorityUtils.createAuthorityList());
SecurityContextHolder.getContext().setAuthentication(apiToken);abc et xyz peuvent être remplacés par une paire d'identifiants uniques tels que phone number et email.
Créez une nouvelle classe Java nommée AuthenticationFilter sous le paquetage filters pour obtenir le contexte authentifié.
package com.example.vonage.auth.filters;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import java.util.Collection;
@Transient
public class AuthenticationFilter extends AbstractAuthenticationToken {
private String apiKey, keySecret;
/**
* Creates a token with the supplied array of authorities.
*
* @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
* represented by this authentication object.
*/
public AuthenticationFilter(String apiKey, String keySecret, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.apiKey = apiKey;
this.keySecret = keySecret;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return keySecret;
}
@Override
public Object getPrincipal() {
return apiKey;
}
}Une fois l'autorisation obtenue, nous transmettrons la demande. filterChain.doFilter(request, response);
Notre code pour le filtre est prêt ! Nous devons maintenant l'utiliser pour chaque requête, à l'exception de la connexion.
Mettons à jour la configuration de Spring Security pour gérer la même chose.
Créez une nouvelle classe Java nommée WebSecurityConfig sous le paquetage config et ajoutez le code suivant.
package com.example.vonage.auth.config;
import com.example.vonage.auth.error.AuthError;
import com.example.vonage.auth.filters.JWTFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthError authErrorHandler;
@Autowired
JWTFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests(configurer ->
configurer.antMatchers("/api/login/**").permitAll().anyRequest().authenticated()
).exceptionHandling().authenticationEntryPoint(authErrorHandler);
}
}Ici, nous avons configuré les éléments cors et csrf pour que l'API fonctionne et nous avons ajouté notre jwtFilter avant tout filtre de sécurité interne à Spring.
UsernamePasswordAuthenticationFilter.class est le premier filtre en priorité. Mais avant cela, notre filtre s'exécute et définit le contexte. Par conséquent, aucune autre vérification n'est nécessaire et la demande est traitée.
Nous avons également défini la session comme étant sans état .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) car nous utilisons un jeton pour l'autorisation.
Toutes les demandes d'authentification sont contournées par le contrôle de sécurité. "/api/login/**".
A la fin, nous avons ajouté une classe d'erreur commune pour gérer l'erreur et fournir une réponse personnalisée .exceptionHandling().authenticationEntryPoint(authErrorHandler);.
Créez une classe Java nommée AuthError sous le paquetage error et ajoutez le code suivant.
package com.example.vonage.auth.error;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class AuthError implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
String responseMsg = mapper.writeValueAsString("Unathorized User");
response.getWriter().write(responseMsg);
response.setContentType("application/json");
}
} Définir des contrôleurs pour les itinéraires restreints et non restreints
Notre filtre et notre validation de jeton sont maintenant en place. Écoutons la requête et authentifions l'utilisateur.
Services
Créer une classe Java nommée "AuthService" dans le paquetage services paquetage.
package com.example.vonage.auth.services;
import com.example.vonage.auth.utils.JWTUtil;
import com.vonage.client.VonageClient;
import com.vonage.client.verify.CheckResponse;
import com.vonage.client.verify.VerifyResponse;
import com.vonage.client.verify.VerifyStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
@Autowired
JWTUtil jwtUtil;
final VonageClient client = VonageClient.builder()
.apiKey("7ed*****")
.apiSecret("W**u*E*VlaWe****")
.build();
public String init(String identifier) {
VerifyResponse response = client.getVerifyClient().verify("91900*******", "Vonage");
if (response.getStatus() == VerifyStatus.OK) {
return response.getRequestId();
}
else {
return "ERROR! " + response.getStatus() + " " + response.getErrorText();
}
}
public String verify(String identifier, String request_id, String otp) {
CheckResponse response = client.getVerifyClient().check(request_id, otp);
if (response.getStatus() == VerifyStatus.OK) {
return jwtUtil.generateToken(identifier);
}
else {
return "Verification failed: " + response.getErrorText();
}
}
}AuthService aura deux méthodes : init et verify.
init accepte le numéro de téléphone comme paramètre et envoie l'OTP à ce numéro de téléphone. La réponse renvoie un request_id qui sera utilisé avec l'OTP pour la validation.
verify accepte le numéro de téléphone, l'identifiant de la demande et l'OTP comme paramètres et vérifie l'identifiant de la demande et l'OTP. Une fois la vérification effectuée, le numéro de téléphone est utilisé comme identifiant, un nouveau jeton est généré et la réponse est renvoyée.
Contrôleurs
Il est possible d'accéder à une application par le biais de points d'accès mis à la disposition de l'utilisateur. Ces points d'accès peuvent être publics, privés ou protégés. Dans notre cas, l'API de connexion sera accessible au public, tandis que toutes les autres API sont protégées et nécessitent une authentification de l'utilisateur pour y accéder.
Auth (Connexion)
Aucune autorisation n'est requise pour accéder à ces itinéraires.
Créez une classe Java nommée "Auth" dans le paquetage controllers et ajoutez le code suivant.
package com.example.vonage.auth.controllers;
import com.example.vonage.auth.services.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
@Validated
@RestController
@RequestMapping("api/login")
public class Auth {
@Autowired
AuthService authService;
@PostMapping(path="/init", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public String init(@NotBlank String identifier){
return authService.sendOtp(identifier);
}
@PostMapping(path="/verify", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public String verify(@NotBlank String identifier, @NotBlank String request_id, @NotBlank String otp) {
return authService.verify(identifier, request_id, otp);
}
} Autres (affectés)
Une autorisation est requise pour accéder à cet itinéraire.
Créez une classe Java nommée "Hello" dans le paquetage controllers et ajoutez le code suivant.
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("api/")
public class Hello {
@GetMapping(path="/hello")
public String hello(){
return "Hello world";
}
} Essais
Nous avons ici trois points d'extrémité de l'API.
/api/hello sans autorisation
Si aucun en-tête Authorization n'est présent dans la requête ou qu'un jeton invalide est fourni, une erreur sera générée, unauthorized user.

/api/login/init
Il accepte le numéro de téléphone au format URL codé et renvoie l'identifiant de la demande après l'envoi de l'OTP.

/api/login/verify
Il accepte le numéro de téléphone, l'identifiant de la demande et l'OTP au format codé en URL et renvoie le jeton JWT après l'envoi de l'OTP.

/api/hello avec autorisation
Dans l'en-tête de la requête, passez
Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5MDA0NzU4NTM0IiwiZXhwIjoxNjYzOTYxMzIwLCJpYXQiOjE2NjM5MjUzMjB9.fY2536Zuz2KNzinZvWU5CNZ6K1p_wOu0pkEbwqP8gbg
Conclusion
Maintenant que nous avons créé un flux de connexion en deux étapes avec le numéro de téléphone et l'OTP, notre prochaine étape pourrait être d'ajouter une authentification à deux facteurs permettant de revérifier le même utilisateur en passant un appel au numéro de téléphone à l'aide de la fonction Voice API de Vonage.
Vonage dispose d'un ensemble intéressant d API intéressantes qui peuvent être utilisées pour faire passer votre produit au niveau supérieur et offrir une meilleure expérience à l'utilisateur.
L'engagement de la communauté est toujours le bienvenu. Rejoignez Vonage sur GitHub pour des exemples de code et la Communauté Slack pour poser des questions à tout moment. Envoyez-nous un tweet et faites-nous part du problème intéressant que vous avez résolu en utilisant l'API de Vonage.
Vous pouvez également me contacter sur Twitter ou sur mon blog learnersbucket.com.
Partager:
Ingénieur frontend senior basé à Mumbai, en Inde. Je travaille sur le développement fullstack avec React et Spring boot pour la Fintech la plus appréciée d'Inde. Je suis également le fondateur de learnersbucket.com où j'écris des guides de préparation aux entretiens JavaScript.