
Compartilhar:
Victor Steven é um desenvolvedor full-stack autodidata que adora pesquisar maneiras diferentes de fazer as coisas. Ele é formado em Engenharia e tem mais de 5 anos de experiência como desenvolvedor de software. Steven tem grande interesse em projetar e criar APIs escaláveis e gosta de escrever sobre suas descobertas no campo do desenvolvimento de software.
Usando JWT para autenticação em um aplicativo em Golang
Tempo de leitura: 14 minutos
Introdução
Um JSON Web Token (JWT) é uma forma compacta e independente de transmitir informações com segurança entre as partes na forma de um objeto JSON, e é comumente utilizado por desenvolvedores em suas APIs. Os JWTs são populares porque:
Um JWT é sem estado. Ou seja, ele não precisa ser armazenado em um banco de dados (camada de persistência), ao contrário dos tokens opacos.
A assinatura de um JWT nunca é decodificada depois de gerada, garantindo assim que o token seja seguro e protegido.
Um JWT pode ser configurado para expirar após um determinado período de tempo. Isso ajuda a minimizar ou eliminar totalmente qualquer dano que possa ser causado por um hacker, caso o token seja comprometido.
Neste tutorial, vou demonstrar como criar, usar e invalidar um JWT em uma API RESTful simples, utilizando Golang e a API de Mensagens da Vonage.
Conta API da Vonage
Para concluir este tutorial, você precisará de uma conta da API da Vonage. Se ainda não tiver uma, pode se cadastrar hoje mesmo e começar a desenvolver usando créditos gratuitos. Assim que tiver uma conta, você poderá encontrar sua chave de API e seu segredo de API na parte superior do Painel da API da Vonage.
Este tutorial também utiliza um número de telefone virtual. Para adquirir um, acesse Números > Comprar Números e procure um que atenda às suas necessidades. Se você acabou de se cadastrar, o custo inicial de um número será facilmente coberto pelo seu crédito disponível.

O que é um JWT?
Um JWT é composto por três partes:
Cabeçalho: o tipo de token e o algoritmo de assinatura utilizado. O tipo de token pode ser “JWT”, enquanto o algoritmo de assinatura pode ser HMAC ou SHA256.
Payload: a segunda parte do token que contém as reivindicações. Essas reivindicações incluem dados específicos da aplicação (por exemplo, identificação do usuário, nome de usuário), tempo de validade do token (expiração), emissor(es), assunto(s) e assim por diante.
Assinatura: o “header” codificado, o “payload” codificado e uma senha fornecida por você são usados para criar a assinatura.
Vamos usar um token simples para entender os conceitos acima.
Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtMNão se preocupe, o token é inválido e, portanto, não funcionará em nenhum aplicativo de produção.
Você pode navegar pelo site jwt.to e testar se a assinatura do token é válida ou não. Use o “HS512” como algoritmo. Você receberá a mensagem “Signature Verified” (Assinatura verificada):
[
]
Para gerar a assinatura, seu aplicativo precisará fornecer uma chave. Essa chave garante que a assinatura permaneça segura — mesmo quando o JWT é decodificado, a assinatura permanece criptografada. É altamente recomendável usar sempre uma senha ao criar um JWT.
Tipos de token
Como um JWT pode ser configurado para expirar (ser invalidado) após um determinado período de tempo, dois tokens serão considerados nesta solicitação:
Token de acesso: Um token de acesso é usado para solicitações que exigem autenticação. Ele é normalmente incluído no cabeçalho da solicitação. Recomenda-se que um token de acesso tenha uma validade curta, digamos, 15 minutos. Atribuir um período curto de validade a um token de acesso pode evitar danos graves caso o token de um usuário seja adulterado ou hackeado. O hacker tem apenas 15 minutos ou menos para realizar suas operações antes que o token seja invalidado.
Token de atualização: Um token de atualização tem uma validade mais longa, geralmente de 7 dias. Esse token é usado para gerar novos tokens de acesso e de atualização. Caso o token de acesso expire, novos conjuntos de tokens de acesso e de atualização são criados quando a rota do token de atualização é acessada (a partir do nosso aplicativo).
Onde armazenar um JWT
Para uma aplicação em ambiente de produção, é altamente recomendável armazenar JWTs em um cookie HttpOnly. Para isso, ao enviar o cookie gerado pelo backend para o frontend (cliente), um sinalizador HttpOnly é enviada junto com o cookie, instruindo o navegador a não exibir o cookie por meio de scripts do lado do cliente. Isso pode prevenir ataques de XSS (Cross Site Scripting). O JWT também pode ser armazenado no armazenamento local do navegador ou no armazenamento de sessão. Armazenar um JWT dessa forma pode expô-lo a vários ataques, como o XSS mencionado acima; por isso, geralmente é menos seguro quando comparado ao uso da técnica do “cookie HttpOnly”.
A Aplicação
Vamos considerar uma API RESTful ToDo.
Crie um diretório chamado jwt-todo, depois inicialize go.mod para gerenciamento das dependências. O go.mod é inicializado usando:
go mod init jwt-todoAgora, crie um arquivo main.go dentro do diretório raiz /jwt-todoe adicione o seguinte a ele:
package main
func main() {}Usaremos o Gin para o roteamento e o tratamento de solicitações HTTP. O Gin Framework ajuda a reduzir o código repetitivo e é muito eficiente na construção de APIs escaláveis.
Você pode instalar o gin, caso ainda não o tenha feito, usando:
go get github.com/gin-gonicEm seguida, atualize o arquivo main.go:
package main
Import (
"github.com/gin-gonic/gin"
)
var (
router = gin.Default()
)
func main() {
router.POST("/login", Login)
log.Fatal(router.Run(":8080"))
}Em uma situação ideal, a rota /login recebe as credenciais de um usuário, compara-as com algum banco de dados e as registra caso sejam válidas. Mas, nesta API, usaremos apenas um usuário de exemplo que definiremos na memória. Crie um usuário de exemplo em uma “struct”. Adicione isso ao arquivo main.go:
type User struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
//A sample use
var user = User{
ID: 1,
Username: "username",
Password: "password",
} Solicitação de login
Quando os dados de um usuário são verificados, ele é autenticado e um JWT é gerado em seu nome. Faremos isso na função Login() definida abaixo:
func Login(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
return
}
//compare the user from the request, with the one we defined:
if user.Username != u.Username || user.Password != u.Password {
c.JSON(http.StatusUnauthorized, "Please provide valid login details")
return
}
token, err := CreateToken(user.ID)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, err.Error())
return
}
c.JSON(http.StatusOK, token)
}
Recebemos a solicitação do usuário e, em seguida, a deserializamos para a “struct” do usuário. Comparamos então o usuário recebido com aquele que definimos na memória. Se estivéssemos utilizando um banco de dados, teríamos comparado com um registro no banco de dados.
Para não tornar a função de login obsoleta, a lógica para gerar um JWT é tratada pela CreateToken. Observe que a identificação do usuário é passada para essa função. Ela é usada como uma reivindicação ao gerar o JWT.
A função CreateToken utiliza o pacote dgrijalva/jwt-go; podemos instalá-lo da seguinte maneira:
go get github.com/dgrijalva/jwt-goVamos definir a função CreateToken:
func CreateToken(userid uint64) (string, error) {
var err error
//Creating Access Token
os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
atClaims := jwt.MapClaims{}
atClaims["authorized"] = true
atClaims["user_id"] = userid
atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix()
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
if err != nil {
return "", err
}
return token, nil
}Definimos que o token seja válido apenas por 15 minutos; logo após esse período, ele será invalidado e não poderá mais ser usado para nenhuma solicitação de autenticação. Observe também que assinamos o JWT utilizando uma senha (ACCESS_SECRET) obtida de nossa variável de ambiente. É altamente recomendável que essa senha não seja exposta em seu código-fonte, mas sim obtida do ambiente, como fizemos acima. Você pode salvá-la em um .env, `.yml ou da maneira que for mais conveniente para você.
Até agora, nosso arquivo main.go está assim:
package main
import (
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"time"
)
var (
router = gin.Default()
)
func main() {
router.POST("/login", Login)
log.Fatal(router.Run(":8080"))
}
type User struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Phone string `json:"phone"`
}
var user = User{
ID: 1,
Username: "username",
Password: "password",
Phone: "49123454322", //this is a random number
}
func Login(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
return
}
//compare the user from the request, with the one we defined:
if user.Username != u.Username || user.Password != u.Password {
c.JSON(http.StatusUnauthorized, "Please provide valid login details")
return
}
token, err := CreateToken(user.ID)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, err.Error())
return
}
c.JSON(http.StatusOK, token)
}
func CreateToken(userId uint64) (string, error) {
var err error
//Creating Access Token
os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
atClaims := jwt.MapClaims{}
atClaims["authorized"] = true
atClaims["user_id"] = userId
atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix()
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
if err != nil {
return "", err
}
return token, nil
}
Agora podemos executar o aplicativo:
go run main.goAgora podemos testá-lo e ver o que conseguimos! Ative sua ferramenta de API preferida e clique no endpoint de login:
[
]
Como visto acima, geramos um JWT com validade de 15 minutos.
Lacunas na implementação
Sim, podemos fazer o login de um usuário e gerar um JWT, mas há muitos erros na implementação acima:
O JWT só pode ser invalidado quando expirar. Uma grande limitação disso é: um usuário pode fazer login e, em seguida, decidir sair imediatamente, mas o JWT do usuário permanece válido até que o prazo de validade seja atingido.
O JWT pode ser hackeado e usado por um hacker sem que o usuário faça nada a respeito, até que o token expire.
O usuário precisará se cadastrar novamente após o vencimento do token, o que resultará em uma experiência negativa para o usuário.
Podemos resolver os problemas mencionados acima de duas maneiras:
Utilizando uma camada de armazenamento persistente para armazenar metadados de JWT. Isso nos permitirá invalidar um JWT no exato momento em que o usuário sair da conta, melhorando assim a segurança.
Utilizando o conceito de atualização do token para gerar um novo token de acesso, caso o token de acesso expire, melhorando assim a experiência do usuário.
Usando o Redis para armazenar metadados de JWT
Uma das soluções que oferecemos acima é salvar os metadados do JWT em uma camada de persistência. Isso pode ser feito em qualquer camada de persistência de sua escolha, mas o Redis é altamente recomendado. Como os JWTs que geramos têm prazo de validade, o Redis possui um recurso que elimina automaticamente os dados cujo prazo de validade foi atingido. O Redis também é capaz de processar um grande volume de gravações e pode ser escalado horizontalmente.
Como o Redis é um banco de dados do tipo chave-valor, suas chaves precisam ser únicas. Para isso, usaremos um UUID como chave e o ID do usuário como valor.
Portanto, vamos instalar dois pacotes para usar:
go get github.com/go-redis/redis/v7
go get github.com/twinj/uuidTambém importaremos os que estão no arquivo main.go assim:
import (
…
"github.com/go-redis/redis/v7"
"github.com/twinj/uuid"
…
)Nota: Espera-se que você tenha o Redis instalado em seu computador. Caso contrário, você pode interromper o processo e fazer a instalação antes de continuar.
Vamos agora iniciar o Redis:
var client *redis.Client
func init() {
//Initializing redis
dsn := os.Getenv("REDIS_DSN")
if len(dsn) == 0 {
dsn = "localhost:6379"
}
client = redis.NewClient(&redis.Options{
Addr: dsn, //redis port
})
_, err := client.Ping().Result()
if err != nil {
panic(err)
}
}
O cliente Redis é inicializado na função init(). Isso garante que, sempre que executarmos o arquivo main.go, o Redis seja conectado automaticamente.
Quando criamos um token a partir deste ponto, geramos um UUID que será usado como um dos claims do token, da mesma forma que usamos o ID do usuário como um claim na implementação anterior.
Definir os metadados=
Na solução que propomos, em vez de criar apenas um token, precisaremos criar dois JWTs:
O token de acesso
Atualização do token
Para isso, será necessário definir uma estrutura que contenha essas definições de tokens, seus prazos de validade e UUIDS:
type TokenDetails struct {
AccessToken string
RefreshToken string
AccessUuid string
RefreshUuid string
AtExpires int64
RtExpires int64
}O prazo de validade e os UUIDs são muito úteis, pois serão utilizados ao salvar metadados simbólicos no Redis.
Agora, vamos atualizar a função CreateToken para que fique assim:
func CreateToken(userid uint64) (*TokenDetails, error) {
td := &TokenDetails{}
td.AtExpires = time.Now().Add(time.Minute * 15).Unix()
td.AccessUuid = uuid.NewV4().String()
td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()
td.RefreshUuid = uuid.NewV4().String()
var err error
//Creating Access Token
os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
atClaims := jwt.MapClaims{}
atClaims["authorized"] = true
atClaims["access_uuid"] = td.AccessUuid
atClaims["user_id"] = userid
atClaims["exp"] = td.AtExpires
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
if err != nil {
return nil, err
}
//Creating Refresh Token
os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf") //this should be in an env file
rtClaims := jwt.MapClaims{}
rtClaims["refresh_uuid"] = td.RefreshUuid
rtClaims["user_id"] = userid
rtClaims["exp"] = td.RtExpires
rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET")))
if err != nil {
return nil, err
}
return td, nil
}
Na função acima, o token de acesso expira após 15 minutos e o token de atualização expira após 7 dias. Você também pode observar que adicionamos um UUID como claim a cada token.
Como o UUID é único sempre que é gerado, um usuário pode criar mais de um token. Isso ocorre quando um usuário está conectado em diferentes dispositivos. O usuário também pode sair de qualquer um dos dispositivos sem que isso o desconecte de todos os demais. Que legal!
Salvando metadados de JWTs
Vamos agora chamar a função que será usada para salvar os metadados dos JWTs:
func CreateAuth(userid uint64, td *TokenDetails) error {
at := time.Unix(td.AtExpires, 0) //converting Unix to UTC(to Time object)
rt := time.Unix(td.RtExpires, 0)
now := time.Now()
errAccess := client.Set(td.AccessUuid, strconv.Itoa(int(userid)), at.Sub(now)).Err()
if errAccess != nil {
return errAccess
}
errRefresh := client.Set(td.RefreshUuid, strconv.Itoa(int(userid)), rt.Sub(now)).Err()
if errRefresh != nil {
return errRefresh
}
return nil
}Passamos pelo TokenDetails que contêm informações sobre o tempo de validade dos JWTs e os UUIDs utilizados na criação dos JWTs. Se o tempo de validade for atingido tanto para o Token de Atualização quanto para o Token de Acesso, o JWT é automaticamente excluído do Redis.
Eu, pessoalmente, uso o Redily, uma interface gráfica para o Redis. É uma boa ferramenta. Você pode dar uma olhada abaixo para ver como os metadados do JWT são armazenados no formato chave-valor.
[
]
Antes de testarmos o login novamente, precisaremos chamar a função CreateAuth() na função Login(). Atualizar a função Login:
func Login(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
return
}
//compare the user from the request, with the one we defined:
if user.Username != u.Username || user.Password != u.Password {
c.JSON(http.StatusUnauthorized, "Please provide valid login details")
return
}
ts, err := CreateToken(user.ID)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, err.Error())
return
}
saveErr := CreateAuth(user.ID, ts)
if saveErr != nil {
c.JSON(http.StatusUnprocessableEntity, saveErr.Error())
}
tokens := map[string]string{
"access_token": ts.AccessToken,
"refresh_token": ts.RefreshToken,
}
c.JSON(http.StatusOK, tokens)
}
Podemos tentar acessar o sistema novamente. Salve o arquivo main.go e execute-o. Quando o Postman realizar o login, já devemos ter feito o seguinte:
[
]
Excelente! Temos tanto o access_token quanto o refresh_token, além de metadados simbólicos armazenados no Redis.
Criando um Todo
Agora podemos processar as solicitações que exigem autenticação usando o JWT.
Uma das solicitações não autenticadas nesta API é a criação de qualquer solicitação.
Primeiro, vamos definir uma estrutura Todo:
type Todo struct {
UserID uint64 `json:"user_id"`
Title string `json:"title"`
}Ao executar qualquer solicitação autenticada, precisamos validar o token enviado no cabeçalho de autenticação para verificar se ele é válido. Precisamos definir algumas funções auxiliares para isso.
Primeiro, precisamos extrair o token do cabeçalho da solicitação usando a função ExtractToken:
func ExtractToken(r *http.Request) string {
bearToken := r.Header.Get("Authorization")
//normally Authorization the_token_xxx
strArr := strings.Split(bearToken, " ")
if len(strArr) == 2 {
return strArr[1]
}
return ""
}Em seguida, verificaremos o token:
func VerifyToken(r *http.Request) (*jwt.Token, error) {
tokenString := ExtractToken(r)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
//Make sure that the token method conform to "SigningMethodHMAC"
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("ACCESS_SECRET")), nil
})
if err != nil {
return nil, err
}
return token, nil
}Chamamos o ExtractToken dentro da função VerifyToken para obter a string do token e, em seguida, verificamos o método de assinatura.
Em seguida, verificaremos a validade desse token — se ele ainda é válido ou se já expirou — usando a função TokenValid:
func TokenValid(r *http.Request) error {
token, err := VerifyToken(r)
if err != nil {
return err
}
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
return err
}
return nil
}
Também vamos extrair os metadados do token, que serão buscados em nosso banco de dados Redis, que configuramos anteriormente. Para extrair o token, definimos a função ExtractTokenMetadata:
func ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
token, err := VerifyToken(r)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
accessUuid, ok := claims["access_uuid"].(string)
if !ok {
return nil, err
}
userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
if err != nil {
return nil, err
}
return &AccessDetails{
AccessUuid: accessUuid,
UserId: userId,
}, nil
}
return nil, err
}
A função ExtractTokenMetadata retorna uma AccessDetails (que é uma estrutura). Essa estrutura contém os metadados (access_uuid e user_id) de que precisaremos para realizar uma busca no Redis. Se, por algum motivo, não for possível obter os metadados desse token, a solicitação é interrompida com uma mensagem de erro.
A estrutura AccessDetails mencionada acima tem a seguinte aparência:
type AccessDetails struct {
AccessUuid string
UserId uint64
}Também mencionamos a consulta aos metadados do token no Redis. Vamos definir uma função que nos permitirá fazer isso:
func FetchAuth(authD *AccessDetails) (uint64, error) {
userid, err := client.Get(authD.AccessUuid).Result()
if err != nil {
return 0, err
}
userID, _ := strconv.ParseUint(userid, 10, 64)
return userID, nil
}FetchAuth() aceita os AccessDetails da função ExtractTokenMetadatae, em seguida, procura no Redis. Se o registro não for encontrado, isso pode significar que o token expirou; portanto, é lançado um erro.
Vamos finalmente chamar a função CreateTodo para entender melhor a implementação das funções acima:
func CreateTodo(c *gin.Context) {
var td *Todo
if err := c.ShouldBindJSON(&td); err != nil {
c.JSON(http.StatusUnprocessableEntity, "invalid json")
return
}
tokenAuth, err := ExtractTokenMetadata(c.Request)
if err != nil {
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
userId, err = FetchAuth(tokenAuth)
if err != nil {
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
td.UserID = userId
//you can proceed to save the Todo to a database
//but we will just return it to the caller here:
c.JSON(http.StatusCreated, td)
}
Como vimos, chamamos o ExtractTokenMetadata para extrair os metadados do JWT que é usado no FetchAuth para verificar se os metadados ainda existem em nosso armazenamento Redis. Se tudo estiver correto, o Todo pode então ser salvo no banco de dados, mas optamos por devolvê-lo ao chamador.
Vamos atualizar o main() para incluir a função CreateTodo:
func main() {
router.POST("/login", Login)
router.POST("/todo", CreateTodo)
log.Fatal(router.Run(":8080"))
}Para testar o CreateTodo, faça o login e copie o access_token e adicione-o ao campo “Bearer Token Field” da seguinte forma:
[
]
Em seguida, adicione um título ao corpo da solicitação para formar um todo e envie uma solicitação POST para o endpoint /todo, conforme mostrado abaixo:
[
]
A tentativa de criar uma tarefa sem acesso será negada:
[
]
Solicitação de logout
Até agora, vimos como um JWT é usado para fazer uma solicitação autenticada. Quando um usuário faz o logout, revogamos/invalidamos instantaneamente seu JWT. Isso é possível apagando os metadados do JWT do nosso armazenamento Redis.
Vamos agora definir uma função que nos permita excluir metadados JWT do Redis:
func DeleteAuth(givenUuid string) (int64,error) {
deleted, err := client.Del(givenUuid).Result()
if err != nil {
return 0, err
}
return deleted, nil
}A função acima apagará o registro no Redis que corresponde ao uuid passado como parâmetro.
A função Logout é assim:
func Logout(c *gin.Context) {
au, err := ExtractTokenMetadata(c.Request)
if err != nil {
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
deleted, delErr := DeleteAuth(au.AccessUuid)
if delErr != nil || deleted == 0 { //if any goes wrong
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
c.JSON(http.StatusOK, "Successfully logged out")
}Na função Logout, primeiro extraímos os metadados do JWT. Se for bem-sucedido, procedemos com a remoção desses metadados, tornando assim o JWT inválido imediatamente.
Antes de testar, atualize o arquivo main.go para incluir o endpoint logout da seguinte forma:
func main() {
router.POST("/login", Login)
router.POST("/todo", CreateTodo)
router.POST("/logout", Logout)
log.Fatal(router.Run(":8080"))
}Insira uma access_token válido associado a um usuário e, em seguida, faça o logout do usuário. Lembre-se de adicionar o access_token ao Authorization Bearer Token e, em seguida, acesse o endpoint de logout:
[
]
Agora o usuário está desconectado, e nenhuma outra solicitação pode ser feita com esse JWT novamente, uma vez que ele é imediatamente invalidado. Essa implementação é mais segura do que esperar que um JWT expire depois que o usuário se desconecta do sistema.
Protegendo rotas autenticadas
Temos duas rotas que exigem autenticação:/login e /logout. No momento, com ou sem autenticação, qualquer pessoa pode acessar essas rotas. Vamos mudar isso.
Precisaremos definir a função TokenAuthMiddleware() para garantir essas rotas:
func TokenAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := TokenValid(c.Request)
if err != nil {
c.JSON(http.StatusUnauthorized, err.Error())
c.Abort()
return
}
c.Next()
}
}Como visto acima, chamamos a função TokenValid() (definida anteriormente) para verificar se o token ainda é válido ou se expirou. A função será usada nas rotas autenticadas para protegê-las.
Vamos agora atualizar o main.go para incluir esse middleware:
func main() {
router.POST("/login", Login)
router.POST("/todo", TokenAuthMiddleware(), CreateTodo)
router.POST("/logout", TokenAuthMiddleware(), Logout)
log.Fatal(router.Run(":8080"))
} Atualizando os tokens
Até o momento, podemos criar, utilizar e revogar JWTs. Em um aplicativo que envolva uma interface de usuário, o que acontece se o token de acesso expirar e o usuário precisar fazer uma solicitação autenticada? O usuário será desautenticado e será obrigado a fazer login novamente? Infelizmente, isso é o que acontecerá. Mas isso pode ser evitado usando o conceito de um token de atualização. O usuário não precisa fazer login novamente.
O token de atualização criado junto com o token de acesso será usado para criar novos pares de tokens de acesso e de atualização.
Usando JavaScript para acessar nossas APIs, podemos atualizar os JWTs facilmente por meio de interceptadores do Axios. Em nossa API, precisaremos enviar uma solicitação POST com um refresh_token como corpo para o endpoint /token/refresh.
Vamos primeiro criar a função Refresh():
func Refresh(c *gin.Context) {
mapToken := map[string]string{}
if err := c.ShouldBindJSON(&mapToken); err != nil {
c.JSON(http.StatusUnprocessableEntity, err.Error())
return
}
refreshToken := mapToken["refresh_token"]
//verify the token
os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf") //this should be in an env file
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
//Make sure that the token method conform to "SigningMethodHMAC"
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("REFRESH_SECRET")), nil
})
//if there is an error, the token must have expired
if err != nil {
c.JSON(http.StatusUnauthorized, "Refresh token expired")
return
}
//is token valid?
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
c.JSON(http.StatusUnauthorized, err)
return
}
//Since token is valid, get the uuid:
claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims
if ok && token.Valid {
refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
if !ok {
c.JSON(http.StatusUnprocessableEntity, err)
return
}
userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, "Error occurred")
return
}
//Delete the previous Refresh Token
deleted, delErr := DeleteAuth(refreshUuid)
if delErr != nil || deleted == 0 { //if any goes wrong
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
//Create new pairs of refresh and access tokens
ts, createErr := CreateToken(userId)
if createErr != nil {
c.JSON(http.StatusForbidden, createErr.Error())
return
}
//save the tokens metadata to redis
saveErr := CreateAuth(userId, ts)
if saveErr != nil {
c.JSON(http.StatusForbidden, saveErr.Error())
return
}
tokens := map[string]string{
"access_token": ts.AccessToken,
"refresh_token": ts.RefreshToken,
}
c.JSON(http.StatusCreated, tokens)
} else {
c.JSON(http.StatusUnauthorized, "refresh expired")
}
}
Embora haja muita coisa acontecendo nessa função, vamos tentar entender o fluxo.
Primeiro, pegamos o
refresh_tokendo corpo da solicitação.Em seguida, verificamos o método de assinatura do token.
Em seguida, verificamos se o token ainda é válido.
O
refresh_uuide ouser_idsão então extraídos; trata-se de metadados usados como claims ao criar o token de atualização.Em seguida, buscamos os metadados no Redis Store e os apagamos usando a
refresh_uidcomo chave.Em seguida, criamos um novo par de tokens de acesso e de atualização, que agora serão usados para futuras solicitações.
Os metadados dos tokens de acesso e de atualização são armazenados no Redis.
Os tokens criados são devolvidos ao chamador.
Por outro lado, se o token de atualização não for válido, o usuário não terá permissão para criar um novo par de tokens. Será necessário fazer um novo login para obter novos tokens.
Em seguida, adicione a rota de atualização de tokens na função main():
router.POST("/token/refresh", Refresh)Testando o empoint com um refresh_token válido:
[![teste}(https://www.nexmo.com/wp-content/uploads/2020/03/image7.png)]
E criamos com sucesso novos pares de fichas. Que legal!! 😎.
Enviar mensagens usando a API de Mensagens da Vonage
Vamos notificar os usuários sempre que eles criarem uma tarefa usando a API de mensagens da Vonage.
Você pode definir sua chave de API e sua senha em uma variável `env` e, em seguida, usá-las neste arquivo da seguinte maneira:
var (
NEXMO_API_KEY = os.Getenv( "your_api_key")
NEXMO_API_SECRET = os.Getenv("your_secret")
)A seguir, definiremos algumas estruturas que contêm informações sobre o remetente, o destinatário e o conteúdo da mensagem:
type Payload struct {
From From `json:"from"`
To To `json:"to"`
Message Message `json:"message"`
}
type From struct {
Type string `json:"type"`
Number string `json:"number"`
}
type To struct {
Type string `json:"type"`
Number string `json:"number"`
}
type Content struct {
Type string `json:"type"`
Text string `json:"text"`
}
type Message struct {
Content Content `json:"content"`
}A seguir, definimos a função para enviar uma mensagem a um usuário, conforme mostrado abaixo:
func SendMessage(username, phone string) (*http.Response, error) {
data := Payload{
From: From{
Type: "sms",
Number: "Nexmo",
},
To: To{
Type: "sms",
Number: phone,
},
Message: Message{
Content: Content{
Type: "text",
Text: "Dear " + username + ", a todo was created from your account just now.",
},
},
}
payloadBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
body := bytes.NewReader(payloadBytes)
req, err := http.NewRequest("POST", "https://api.nexmo.com/v0.1/messages", body)
if err != nil {
return nil, err
}
//Ensure headers
req.SetBasicAuth(NEXMO_API_KEY, NEXMO_API_SECRET)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return resp, nil
}Na função acima, o número “To” é o número do usuário, enquanto o número “From” deve ser adquirido por meio do seu painel de controle da Vonage.
Certifique-se de ter suas variáveis NEXMO_API_KEY e NEXMO_API_SECRET definidas em seu arquivo de variáveis de ambiente.
Atualizamos, então, a função CreateTodo para incluir a função SendMessage que acabou de ser definida, passando os parâmetros necessários:
func CreateTodo(c *gin.Context) {
var td *Todo
if err := c.ShouldBindJSON(&td); err != nil {
c.JSON(http.StatusUnprocessableEntity, "invalid json")
return
}
tokenAuth, err := ExtractTokenMetadata(c.Request)
if err != nil {
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
userId, err = FetchAuth(tokenAuth)
if err != nil {
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
td.UserID = userId
//you can proceed to save the Todo to a database
//but we will just return it to the caller here:
//Send the user a notification
msgResp, err := SendMessage(user.Username, user.Phone)
if err != nil {
c.JSON(http.StatusForbidden, "error occurred sending message to user")
return
}
if msgResp.StatusCode > 299 {
c.JSON(http.StatusForbidden, "cannot send message to user")
return
}
c.JSON(http.StatusCreated, td)
}
Certifique-se de fornecer um número de telefone válido para que você possa receber a mensagem ao tentar criar uma conta.
Conclusão
Você viu como é possível criar e invalidar um JWT. Você também viu como integrar a Messages API do Vonage à sua aplicação em Golang para enviar notificações. Para mais informações sobre as melhores práticas e o uso de um JWT, não deixe de conferir este repositório do GitHub. Você pode ampliar essa aplicação e usar um banco de dados real para armazenar usuários e outros dados, além de poder usar React ou Vue.js para construir um front-end. É aí que você realmente vai apreciar o recurso Refresh Token com a ajuda dos Axios Interceptors.
Compartilhar:
Victor Steven é um desenvolvedor full-stack autodidata que adora pesquisar maneiras diferentes de fazer as coisas. Ele é formado em Engenharia e tem mais de 5 anos de experiência como desenvolvedor de software. Steven tem grande interesse em projetar e criar APIs escaláveis e gosta de escrever sobre suas descobertas no campo do desenvolvimento de software.