Skip to content

JWT

JWT tornou-se uma das principais formas de autenticação para comunicação entre servidores modernos, sendo leve e sem estado.

Introdução

JWT, nome completo JSON Web Tokens, sobre a introdução detalhada de JWT pode ser vista em jwt.io. É uma maneira aberta, segura e compacta de transmitir informações entre duas partes usando objetos JSON como meio. Suas características são alta segurança, prevenção de adulteração de conteúdo e baixo consumo.

Estrutura

No padrão RFC, o JWT consiste nas seguintes três partes:

  • Header (Cabeçalho)
  • Payload (Carga)
  • Signature (Assinatura)

Cada parte é separada por um ponto . e forma uma string. O formato é header.payload.signature, que é a estrutura padrão de um token JWT. A seguir, explicaremos a função de cada estrutura.

TIP

Note que base64 e base64URL não são o mesmo tipo de codificação. Este último é compatível com URLs da web e realiza escape.

Cabeçalho

O cabeçalho apenas declara algumas informações básicas, geralmente consistindo de duas partes: o tipo de token e o algoritmo de criptografia usado para a assinatura. Por exemplo:

json
{
  "alg": "HS256",
  "typ": "JWT"
}

As informações acima são basicamente: o tipo de token é JWT, e o algoritmo de criptografia usado na parte de assinatura é HS256. Em seguida, o objeto JSON é codificado em uma string usando Base64Url, e essa string é o cabeçalho JWT.

Payload

A segunda parte do JWT é a parte de payload, que contém principalmente declarações (claims). As declarações geralmente são dados sobre uma entidade, como um usuário. Existem três tipos de declarações:

  • registered: Registered claims são algumas declarações pré-definidas. Não são obrigatórias, mas ainda recomendadas. Exemplos: iss (emissor), exp (tempo de expiração), aud (público-alvo).
  • public: Public claims podem ser definidas livremente pelos usuários de JWT. É melhor evitar conflitos com outras declarações.
  • private claims: Esta parte das declarações também é personalizada, geralmente usada para compartilhar informações entre as duas partes do serviço.

Um exemplo de payload:

json
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Este objeto JSON será codificado em uma string usando Base64Url, formando a segunda parte do JWT.

DANGER

Embora a parte de payload também seja protegida e tenha prevenção de adulteração, esta parte é publicamente legível. Portanto, não coloque informações sensíveis dentro do JWT.

Assinatura

Após obter o cabeçalho codificado e a payload codificada, pode-se usar o algoritmo de assinatura indicado no cabeçalho para criptografar e assinar com base no conteúdo das duas primeiras partes mais a chave secreta. Portanto, uma vez que o conteúdo do JWT muda, a assinatura obtida durante a descriptografia será diferente. Se usar uma chave privada, também é possível verificar o emissor do JWT.

sign = HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Por exemplo:

Header
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload
{
  "alg": "HS256",
  "typ": "JWT"
}

Verify Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your secret
)

O resultado final é uma string composta por três strings base64Url separadas por ., algo como:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ

Como Funciona

Na autenticação, quando um usuário faz login com sucesso usando credenciais, um JSON Web Token é retornado. Como o token é uma credencial, deve-se ter muito cuidado para evitar problemas de segurança. Em geral, o token não deve ser mantido por mais tempo do que o necessário. Então, sempre que o usuário quiser acessar rotas e recursos protegidos, ao fazer a requisição, é necessário incluir o token, geralmente no cabeçalho Authorization usando o esquema Bearer, como:

Authorization: Bearer <token>

Após receber o JWT, o servidor verificará sua validade, como adulteração de conteúdo, expiração do token, etc. Se a verificação for bem-sucedida, o acesso aos recursos será permitido. Embora o JWT possa conter algumas informações básicas, ainda é recomendado que as informações não sejam muito grandes.

Biblioteca JWT

Repositório oficial: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go

Documentação oficial: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages

Esta biblioteca suporta análise, validação, geração e assinatura de JWT. Atualmente, os algoritmos de assinatura suportados são HMAC SHA, RSA, RSA-PSS e ECDSA, mas também é possível adicionar seus próprios ganchos.

Instalação

go get -u github.com/golang-jwt/jwt/v4

Importação

go
import "github.com/golang-jwt/jwt/v4"

Escolha do Algoritmo de Assinatura

Existem vários algoritmos de assinatura disponíveis. Antes de usar, é bom entender as diferenças entre eles para escolher melhor o algoritmo de assinatura. A maior diferença entre eles é criptografia simétrica e criptografia assimétrica.

O algoritmo de criptografia simétrica mais simples é HSA, que permite que qualquer []byte seja usado como uma chave válida, então a velocidade de cálculo é um pouco mais rápida. Quando ambas as partes produtora e consumidora são confiáveis, a eficiência dos algoritmos de criptografia simétrica é a mais alta. No entanto, como a assinatura e a verificação usam a mesma chave, não é fácil distribuir a chave usada para verificação, já que a chave de assinatura é a mesma. Se a chave de assinatura vazar, a segurança do JWT se torna sem sentido.

Métodos de assinatura de criptografia assimétrica, como RSA, usam chaves diferentes para assinar e verificar o token. Isso torna possível gerar tokens com uma chave privada e também permite que qualquer pessoa com a chave pública verifique o acesso.

Diferentes algoritmos de assinatura requerem diferentes tipos de chaves. Abaixo estão alguns tipos de algoritmos de assinatura comuns:

  • HMAC: Criptografia simétrica, requer um valor do tipo []byte para assinar e verificar. (HS256, HS384, HS512)
  • RSA: Criptografia assimétrica, requer um valor do tipo *rsa.PrivateKey para assinar e *rsa.PublicKey para verificar. (RS256, RS384, RS512)
  • ECDSA: Criptografia assimétrica, requer um valor do tipo *ecdsa.PrivateKey para assinar e *ecdsa.PublicKey para verificar. (ES256, ES384, ES512)
  • EdDSA: Criptografia assimétrica, requer um valor do tipo ed25519.PrivateKey para assinar e ed25519.PublicKey para verificar. (Ed25519)

Exemplos

A seguir, serão demonstrados alguns exemplos sobre criação e assinatura de JWT, bem como análise e verificação.

go
type Token struct {
  Raw       string                 // String original do Token, preenchida ao iniciar a análise
  Method    SigningMethod          // Método usado para assinatura
  Header    map[string]interface{} // Parte header do JWT
  Claims    Claims                 // Parte payload do JWT
  Signature string                 // Parte de assinatura do JWT, preenchida ao iniciar a análise
  Valid     bool                   // Se o JWT é válido
}

A estrutura Token representa um Token JWT. O uso dos campos depende principalmente de como o JWT é criado/assinado ou analisado/verificado.

go
type RegisteredClaims struct {
   // a claim `iss` (Issuer). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
   Issuer string `json:"iss,omitempty"`

   // a claim `sub` (Subject). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
   Subject string `json:"sub,omitempty"`

   // a claim `aud` (Audience). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
   Audience ClaimStrings `json:"aud,omitempty"`

   // a claim `exp` (Expiration Time). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
   ExpiresAt *NumericDate `json:"exp,omitempty"`

   // a claim `nbf` (Not Before). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
   NotBefore *NumericDate `json:"nbf,omitempty"`

   // a claim `iat` (Issued At). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
   IssuedAt *NumericDate `json:"iat,omitempty"`

   // a claim `jti` (JWT ID). Veja https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
   ID string `json:"jti,omitempty"`
}

Estas são as Claims pré-definidas fornecidas pela biblioteca, que podem ser usadas adequadamente para atender às necessidades.

Exemplo 1. Criação e Assinatura com HMAC

go
func TestHmac(t *testing.T) {
   // O tipo de chave secreta para hmac é array de bytes
   secret := []byte("my secret")
   // Usando algoritmo HS256, jwt.MapClaims é o payload
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "id":   123456,
      "name": "jack",
   })
   fmt.Printf("%+v\n", *token)
   // Assinatura
   signedString, err := token.SignedString(secret)
   fmt.Println(signedString, err)
}

Saída:

{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>

Exemplo 2. Usando Claims Pré-definidas

go
mySigningKey := []byte("AllYourBase")

// Criar Claims
claims := &jwt.RegisteredClaims{
  ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
  Issuer:    "test",
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)

Saída:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>

Exemplo 3. Claims Personalizadas

go
type MyClaims struct {
   User string `json:"user"`
   jwt.RegisteredClaims
}

func TestCustomClaims(t *testing.T) {
   // Criar chave secreta
   secret := []byte("my secret")
   // Criar Claims
   claims := MyClaims{
      User: "114514",
      RegisteredClaims: jwt.RegisteredClaims{
         ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
         IssuedAt:  jwt.NewNumericDate(time.Now()),
         NotBefore: jwt.NewNumericDate(time.Now()),
         Issuer:    "Server",
      },
   }
   // Criar Token
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   // Assinatura
   signedString, err := token.SignedString(secret)
   fmt.Println(signedString, err)
}

Saída:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>

TIP

Ao incorporar Claims padrão em Claims personalizadas, certifique-se de:

  1. O Claims padrão incorporado é do tipo não ponteiro

  2. Se for do tipo ponteiro, é melhor garantir que a memória adequada seja alocada antes de passar, caso contrário ocorrerá panic.

Exemplo 4. Análise e Verificação de Token HMAC

go
func TestParse(t *testing.T) {
   secret := []byte("my secret")
   // Suponha que um token foi criado e assinado usando o algoritmo HS256
   tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"

   // Passe a string do token e a função de verificação. O valor de retorno é uma estrutura Token
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
      // Verificar se o algoritmo de assinatura corresponde
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("Algoritmo de assinatura não correspondente: %s", token.Header["alg"])
      }

      // Retornar chave de verificação
      return secret, nil
   })
   if err != nil {
      fmt.Println(token, err)
   }

   if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
      fmt.Println(claims)
   } else {
      fmt.Println(err)
   }

}

Saída:

map[id:123456 name:jack]

Exemplo 5. Tratamento de Erros

go
func TestProcess(t *testing.T) {
   secret := []byte("my secret")
   // Suponha que um token foi criado e assinado usando o algoritmo HS256
   tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"

   // Passe a string do token e a função de verificação. O valor de retorno é uma estrutura Token
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
      // Verificar se o algoritmo de assinatura corresponde
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("Algoritmo de assinatura não correspondente: %s", token.Header["alg"])
      }
      // Retornar chave de verificação
      return secret, nil
   })

   if token.Valid {
      fmt.Println("token válido")
   } else if errors.Is(err, jwt.ErrTokenMalformed) {
      fmt.Println("A string passada nem mesmo é um token...")
   } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
      fmt.Println("O token expirou ou ainda não é válido")
   } else {
      fmt.Println("Processamento de token anômalo...")
   }
}

Saída:

token válido

Exemplo 6. Análise de Claims Personalizadas

Se Claims personalizadas foram usadas ao criar o token, e espera-se que as Claims sejam convertidas para Claims personalizadas em vez de map durante a análise, é necessário passar Claims personalizadas.

go
func TestCustomClaimsParse(t *testing.T) {
  secret := []byte("my secret")
  tokenstring := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg4MDU2LCJuYmYiOjE2NzMwODQ0NTYsImlhdCI6MTY3MzA4NDQ1Nn0.T245aoDeL2x19X8_JZde0EmZ2TDyIgr1u3ddKFjQmgw"
  token, err := jwt.ParseWithClaims(tokenstring, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
    return secret, nil
  }, jwt.WithValidMethods([]string{"HS256"})) // Usar option para verificação

    // Type assertion
  if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
    fmt.Println(claims)
  } else {
    fmt.Println(err)
  }
}

Saída:

&{114514 {Server  [] 2023-01-07 18:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST }}

Exemplo 7. Assinatura e Análise com RSA

RSA é mais comumente usado em arquiteturas distribuídas. O processo é aproximadamente o seguinte:

  1. O centro de autenticação cria um par de chaves, usa a chave privada para assinar o JWT, que é retornado ao cliente. A chave pública é mantida pelo serviço de negócios.
  2. O cliente faz uma requisição ao serviço de negócios com o JWT. O módulo de negócios usa a chave pública para analisar o JWT, sem precisar acessar o centro de autenticação.
  3. Se a autenticação for bem-sucedida, retorna as informações de negócios.
  4. Se falhar, retorna informações de falha.
go
func TestRsa(t *testing.T) {

   // Criar par de chaves
   privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
   publicKey := &privateKey.PublicKey

   if err != nil {
      fmt.Println(err)
      return
   }

   // claims
   claims := MyClaims{
      User: "114514",
      RegisteredClaims: jwt.RegisteredClaims{
         ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
         IssuedAt:  jwt.NewNumericDate(time.Now()),
         NotBefore: jwt.NewNumericDate(time.Now()),
         Issuer:    "Server",
      },
   }

   token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
   // Criptografia com chave privada
   signedString, err := token.SignedString(privateKey)

   fmt.Println(signedString, err)

   // Descriptografia com chave pública
   token, err = jwt.ParseWithClaims(signedString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
      return publicKey, nil
   })

   if err != nil {
      fmt.Println(err)
   } else if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
      fmt.Println(claims)
   }
}

Golang por www.golangdev.cn edit