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:
{
"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 claimssã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 claimspodem 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:
{
"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_xSJyQQComo 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/v4Importação
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[]bytepara assinar e verificar. (HS256,HS384,HS512)RSA: Criptografia assimétrica, requer um valor do tipo*rsa.PrivateKeypara assinar e*rsa.PublicKeypara verificar. (RS256,RS384,RS512)ECDSA: Criptografia assimétrica, requer um valor do tipo*ecdsa.PrivateKeypara assinar e*ecdsa.PublicKeypara verificar. (ES256,ES384,ES512)EdDSA: Criptografia assimétrica, requer um valor do tipoed25519.PrivateKeypara assinar eed25519.PublicKeypara verificar. (Ed25519)
Exemplos
A seguir, serão demonstrados alguns exemplos sobre criação e assinatura de JWT, bem como análise e verificação.
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.
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
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
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
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:
O Claims padrão incorporado é do tipo não ponteiro
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
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
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álidoExemplo 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.
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:
- 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.
- 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.
- Se a autenticação for bem-sucedida, retorna as informações de negócios.
- Se falhar, retorna informações de falha.
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)
}
}