
Compartilhar:
Engenheiro sênior de front-end, residindo em Mumbai, na Índia. Trabalho com desenvolvimento full-stack usando React e Spring Boot para a empresa de fintech mais valorizada da Índia. Também sou o fundador do learnersbucket.com, onde escrevo guias de preparação para entrevistas sobre JavaScript.
Autenticação baseada em OTP (JWT) no Spring Boot com a Verify API de Vonage
Tempo de leitura: 7 minutos
Este artigo foi atualizado em março de 2025
Visão geral
Uma experiência do usuário (UX) integrada é um fator determinante para o crescimento do produto. A autenticação é parte integrante de uma boa experiência do usuário, especialmente em applications bancárias ou de FinTech.
Imagine ter que criar uma página de login para um banco em que o cliente use um número de telefone e uma senha de uso único (OTP) para autenticação. Eliminar a necessidade de lembrar uma senha e, ao mesmo tempo, oferecer segurança melhoraria a experiência do usuário.
Vamos ver como podemos criar uma autenticação JWT (JSON Web Token) baseada em OTP em Java. Usaremos o framework Spring Boot e a Verify API.
Saiba mais sobre a JWT aqui.
Account da API da Vonage
Antes de começarmos a usar a API, precisaremos de uma Account na API da Vonage.

Assim que tivermos nossa chave de API e segredo da API, vamos usá-las para a Verify API.
Configuração
Vamos criar um novo aplicativo Spring e importaremos o SDK do servidor Java da Vonage para que possamos usar as APIs da Vonage em nosso aplicativo. Adicionaremos o código abaixo ao nosso build.gradle ou no arquivo POM.
Gradle
Adicione o seguinte ao build.gradle arquivo.
repositories {
mavenCentral()
}
dependencies {
implementation 'com.vonage:server-sdk:8.20.1'
} Maven
Adicione o seguinte ao arquivo POM do nosso projeto.
<dependency>
<groupid>com.vonage</groupid>
<artifactid>server-sdk</artifactid>
<version>8.20.1</version>
</dependency> Visão geral da implementação
Agora que concluímos a configuração, podemos começar o desenvolvimento. Essa autenticação de login OTP com JWT pode ser realizada em três etapas:
JWT Creation
Filtrar solicitações de usuários
Funcionalidade da API de acesso
JWT Creation
Utilizaremos um mecanismo de autorização baseado em tokens, no qual, assim que o usuário for autenticado com sucesso, geraremos um novo token com um prazo de validade e o devolveremos ao usuário.
O usuário deverá enviar esse token em cada solicitação para comprovar sua identidade e, assim, ter acesso às Applications que exigem autorização.
Filtrar solicitações de usuários
Sempre que recebermos um token na solicitação, devemos verificá-lo e informar ao Spring Security que o usuário está autorizado e pode acessar os endpoints restritos. Precisaremos de um filtro para cada solicitação.
Funcionalidade da API de acesso
Usaremos três rotas de API diferentes para concluir a autenticação. Cada uma delas tem uma finalidade específica:
Obtenha o número de telefone do usuário para enviar a senha de uso único (OTP)
Verify a OTP e retorne o JWT ao usuário
Parâmetro geral para testes
Vamos ver como cada um deles pode ser implementado separadamente.
Como lidar com a criptografia do JWT
O JWT é uma combinação de dois métodos de criptografia diferentes, criados com base no JWS e no JWE, que podem ser criptografados por meio de uma chave simétrica SECRET_KEY com uma carga útil.
A carga útil pode conter dados confidenciais que podemos usar para validar o usuário (por exemplo, data de validade, detalhes do usuário etc.)
Usando o mesmo SECRET_KEY, podemos descriptografar o token para obter a carga útil e usá-la quando necessário.
Para lidar com JWT, usaremos o io.jsonwebtoken::jjwt pacote.
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
}Crie uma JWTUtils classe no util pacote e adicione o código a seguir.
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);
}
}Isso contém toda a lógica relacionada ao JWT, que pode ser usada para criar um novo token, validar o token e obter identificadores a partir do token para validar o usuário, etc.
Aqui, substituímos o SECRET_KEY por qualquer chave secreta compartilhada e acessamos essa chave a partir dos arquivos de propriedades do aplicativo, em vez de mantê-la codificada no código-fonte.
Além disso, ao gerar o token, passamos os detalhes (número de telefone) como uma string, mas você pode armazenar qualquer objeto e extraí-lo.
Mantivemos a validação extremamente simples: estamos apenas armazenando o número de telefone no token, e esse mesmo número será usado para autorização todas as vezes.
No validateToken método, comparamos os números de telefone e verificamos se o token expirou.
A data de validade do token é de dez dias Duration.ofDays(10).toMillis() a partir do momento da criação do token.
Adicione um filtro para autenticar um usuário e definir o contexto
Sempre que recebermos qualquer solicitação que exija autorização, teremos que validar o token e definir o contexto de que o usuário está autenticado.
Vamos adicionar um novo filtro que ampliará o OncePerRequestFilter– como o nome sugere; esse filtro será executado uma vez para cada solicitação.
Crie uma nova classe Java chamada JWTFilter dentro do filters pacote e adicione o código a seguir.
Isso conterá a lógica para extrair o token da solicitação e validá-lo. Se houver autorização, defina o contexto indicando que o usuário está autenticado.
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);
}
}Em cada solicitação, estamos obtendo o Authorization cabeçalho e extraímos o token dele após o texto Bearer, e é por isso que estamos obtendo a subcadeia após os primeiros sete caracteres (incluindo um espaço após a palavra Bearer).
Assim que tivermos o token, extraímos o identificador dele para validação.
Para validação, estamos verificando se o usuário já está autenticado ou não, o que não será o caso, pois estamos lidando com um SESSÃO SEM ESTADO e se o identificador é o mesmo e se o token não expirou. A sessão é sem estado porque não estamos mantendo nenhuma sessão, já que se trata de uma autorização baseada em token. Essa configuração será atualizada no Spring Security.
Como usamos apenas o número de telefone como identificador, não há verificação cruzada. Podemos tornar o sistema mais seguro armazenando o e-mail do usuário ou qualquer outro objeto exclusivo e, em seguida, usando o número de telefone para recuperar a mesma informação do banco de dados e realizar a verificação.
Para obter o contexto de autenticação, estendemos o AbstractAuthenticationToken e fornecemos uma chave e um segredo exclusivos.
AuthenticationFilter apiToken = new AuthenticationFilter("abc", "xyz", AuthorityUtils.createAuthorityList());
SecurityContextHolder.getContext().setAuthentication(apiToken);abc e xyz pode ser substituído por um par de identificação exclusivo, como phone number e email.
Crie uma nova classe Java chamada AuthenticationFilter no pacote filters pacote para obter o contexto Authenticated.
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;
}
}Assim que a autorização for concluída, encaminharemos a solicitação adiante filterChain.doFilter(request, response);
Nosso código para o filtro está pronto! Agora precisamos usá-lo em todas as solicitações, exceto na de login.
Vamos atualizar a configuração do Spring Security para lidar com isso.
Crie uma nova classe Java chamada WebSecurityConfig no config pacote e adicione o código a seguir.
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);
}
}Aqui, configuramos o cors e csrf para que a API funcione e adicionamos nosso jwtFilter antes de qualquer filtro interno do Spring Security.
UsernamePasswordAuthenticationFilter.class é o primeiro filtro em ordem de prioridade. Mas, antes disso, nosso filtro é executado e define o contexto. Portanto, não são necessárias mais verificações e a solicitação segue sendo processada.
Também configuramos a sessão para ser sem estado .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) , já que estamos usando um token para autorização.
Todas as solicitações de autenticação são ignoradas na verificação de segurança "/api/login/**".
No final, adicionamos uma classe de erro comum para lidar com o erro e fornecer uma resposta personalizada .exceptionHandling().authenticationEntryPoint(authErrorHandler);.
Crie uma classe Java chamada AuthError no error pacote e adicione o código a seguir.
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");
}
} Definir controladores para as rotas restritas e irrestritas
Nosso filtro e a validação do token já estão em funcionamento. Vamos analisar a solicitação e autenticar o usuário.
Serviços
Crie uma classe Java chamada 'AuthService' no services pacote.
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 terá dois métodos: init e Verify.
init aceita o número de telefone como parâmetro e envia a OTP para esse número. A resposta retornará um request_id que será usado com a OTP para validação.
verify aceitará o número de telefone, o request_id e a OTP como parâmetros e verificará o request_id e a OTP. Após a verificação, utilizará o número de telefone como identificador, gerará um novo token e retornará a resposta.
Controladores
É possível acessar uma aplicação por meio de endpoints disponibilizados ao usuário. Os endpoints podem ser públicos, privados ou protegidos. No nosso caso, a API de login estará disponível publicamente, enquanto todas as outras APIs são protegidas e exigem autenticação do usuário para serem acessadas.
Autenticação (Login)
Não é necessária nenhuma autorização para acessar essas rotas.
Crie uma classe Java chamada 'Auth' no controllers pacote e adicione o código a seguir.
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);
}
} Outros (Restrito)
É necessária autorização para acessar esta rota.
Crie uma classe Java chamada “Hello” no controllers pacote e adicione o código a seguir.
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";
}
} Testes
Aqui temos três pontos de extremidade da API.
/api/hello sem autorização
Se nenhum Authorization cabeçalho estiver presente na solicitação ou for fornecido um token inválido, será gerado um erro, unauthorized user.

/api/login/init
Ele aceita o número de telefone no formato codificado como URL e retorna o ID da solicitação após enviar a senha de uso único (OTP).

/api/login/verify
Ele aceita o número de telefone, o ID da solicitação e a OTP no formato codificado como URL e retorna o token JWT após enviar a OTP.

/api/hello com autorização
No cabeçalho da solicitação, passe
Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5MDA0NzU4NTM0IiwiZXhwIjoxNjYzOTYxMzIwLCJpYXQiOjE2NjM5MjUzMjB9.fY2536Zuz2KNzinZvWU5CNZ6K1p_wOu0pkEbwqP8gbg
Conclusão
Agora que criamos um fluxo de login em duas etapas com número de telefone e OTP, nosso próximo passo poderia ser adicionar a autenticação de dois fatores, na qual podemos verificar novamente o mesmo usuário fazendo uma ligação para o número de telefone usando a Voice API da Vonage.
A Vonage possui um conjunto interessante de APIs disponíveis que podem ser usadas para elevar seu produto a um novo patamar e proporcionar uma melhor experiência ao usuário.
A participação da comunidade é sempre bem-vinda. Junte-se à Vonage no GitHub para ver exemplos de código e no Slack da Comunidade para tirar dúvidas a qualquer momento. Envie-nos um tweet e conte-nos sobre o problema interessante que você resolveu usando a API da Vonage.
Você também pode entrar em contato comigo pelo Twitter ou pelo meu blog learnersbucket.com.
Compartilhar:
Engenheiro sênior de front-end, residindo em Mumbai, na Índia. Trabalho com desenvolvimento full-stack usando React e Spring Boot para a empresa de fintech mais valorizada da Índia. Também sou o fundador do learnersbucket.com, onde escrevo guias de preparação para entrevistas sobre JavaScript.