JWT
JWT è diventato uno dei principali metodi di autenticazione per le comunicazioni server moderne con caratteristiche di leggerezza e statelessness.
Introduzione
JSON Web Tokens, per ulteriori informazioni su JWT è possibile visitare jwt.io. È un modo aperto, sicuro e compatto per trasmettere informazioni tra le parti del servizio utilizzando oggetti JSON come supporto. Le sue caratteristiche includono elevata sicurezza, prevenzione della manomissione dei contenuti e basso consumo.
Struttura
Nello standard RFC, JWT è composto dalle seguenti tre parti:
- Header (intestazione)
- Payload (carico utile)
- Signature (firma)
Ogni parte è separata da un punto . per formare una stringa. Il formato è header.payload.signature, questa è la struttura standard di un token JWT. Di seguito verrà spiegata la funzione di ciascuna struttura.
TIP
Va notato che base64 e base64URL non sono lo stesso tipo di codifica; quest'ultima è compatibile con gli URL web e li converte.
Header
L'header dichiara solo alcune informazioni di base, solitamente composte da due parti: il tipo di token e l'algoritmo di crittografia utilizzato per la firma, come segue:
{
"alg": "HS256",
"typ": "JWT"
}Le informazioni sopra indicano approssimativamente che il tipo di token è JWT e l'algoritmo di crittografia utilizzato per la parte di firma è HS256. Infine, l'oggetto JSON viene codificato in una stringa tramite Base64Url, che costituisce l'header JWT.
Payload
La seconda parte di JWT è la parte payload, che contiene principalmente le dichiarazioni (claims). Le dichiarazioni sono solitamente dati su un'entità, come un utente. Esistono tre tipi di dichiarazioni:
registered: IRegistered claimsrappresentano alcune dichiarazioni predefinite. Non sono obbligatorie ma sono comunque raccomandate, ad esempio:iss(emittente),exp(tempo di scadenza),aud(destinatario).public: IPublic claimspossono essere definiti liberamente dagli utenti di JWT, meglio evitare conflitti con altre dichiarazioni.private claims: Questa parte delle dichiarazioni è anche personalizzata, solitamente utilizzata per condividere informazioni tra le parti del servizio.
Un esempio di payload è il seguente:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}L'oggetto JSON verrà codificato in una stringa tramite Base64Url, formando così la seconda parte di JWT.
DANGER
Sebbene la parte payload sia anche protetta e abbia prevenzione di manomissione, questa parte è pubblicamente leggibile, quindi non inserire informazioni sensibili all'interno di JWT.
Signature
Dopo aver ottenuto l'header codificato e il payload codificato, è possibile utilizzare l'algoritmo di firma indicato nell'header per crittografare e firmare il contenuto in base alle prime due parti più una chiave segreta. Quindi, una volta che il contenuto di JWT subisce qualsiasi modifica, la firma ottenuta durante la decrittografia sarà diversa. Se si utilizza una chiave privata, è anche possibile verificare l'emittente di JWT.
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)Ad esempio:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)L'output finale è una stringa composta da tre stringhe base64Url separate da ., approssimativamente come segue:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQCome Funziona
Nell'autenticazione, quando un utente esegue correttamente l'accesso con le credenziali, verrà restituito un JSON Web Token. Poiché il token è una credenziale, deve essere gestito con molta attenzione per prevenire problemi di sicurezza. In generale, il token non dovrebbe essere conservato più a lungo del necessario. Quando l'utente desidera accedere a route e risorse protette, deve includere il token nella richiesta, solitamente nell'header Authorization con lo schema Bearer, come segue:
Authorization: Bearer <token>Dopo aver ricevuto il JWT, il server ne verificherà la validità, ad esempio se il contenuto è stato manomesso o se il token è scaduto. Se la verifica ha successo, sarà possibile accedere alle risorse. Sebbene JWT possa trasportare alcune informazioni di base, si consiglia comunque di non rendere le informazioni troppo grandi.
Libreria JWT
Repository ufficiale: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
Documentazione ufficiale: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
Questa libreria supporta l'analisi, la verifica, la generazione e la firma di JWT. Gli algoritmi di firma attualmente supportati sono HMAC SHA, RSA, RSA-PSS e ECDSA, ma è anche possibile aggiungere i propri hook.
Installazione
go get -u github.com/golang-jwt/jwt/v4Importazione
import "github.com/golang-jwt/jwt/v4"Scelta dell'Algoritmo di Firma
Esistono diversi algoritmi di firma disponibili. Prima dell'uso, è necessario comprendere le differenze tra di essi per scegliere meglio l'algoritmo di firma. La differenza principale è tra crittografia simmetrica e crittografia asimmetrica.
L'algoritmo di crittografia simmetrica più semplice è HSA, che consente a qualsiasi []byte di essere utilizzato come chiave valida, quindi la velocità di calcolo è leggermente più veloce. L'efficienza degli algoritmi di crittografia simmetrica è massima quando entrambe le parti produttrici e consumatrici sono attendibili. Tuttavia, poiché la firma e la verifica utilizzano la stessa chiave, non è facile distribuire le chiavi per la verifica, dato che la chiave di firma è la stessa. Se la chiave di firma viene compromessa, la sicurezza di JWT diventa priva di senso.
I metodi di firma asimmetrica, come RSA, utilizzano chiavi diverse per firmare e verificare il token, rendendo possibile generare token con una chiave privata e consentendo a chiunque abbia la chiave pubblica di verificare l'accesso.
Diversi algoritmi di firma richiedono diversi tipi di chiavi. Di seguito sono riportati alcuni tipi di algoritmi di firma comuni:
HMAC: Crittografia simmetrica, richiede un valore di tipo[]byteper la firma e la verifica. (HS256,HS384,HS512)RSA: Crittografia asimmetrica, richiede un valore di tipo*rsa.PrivateKeyper la firma e un valore di tipo*rsa.PublicKeyper la verifica. (RS256,RS384,RS512)ECDSA: Crittografia asimmetrica, richiede un valore di tipo*ecdsa.PrivateKeyper la firma e un valore di tipo*ecdsa.PublicKeyper la verifica. (ES256,ES384,ES512)EdDSA: Crittografia asimmetrica, richiede un valore di tipoed25519.PrivateKeyper la firma e un valore di tipoed25519.PublicKeyper la verifica. (Ed25519)
Esempi
Di seguito verranno mostrati alcuni esempi sulla creazione e firma di jwt, nonché sull'analisi e verifica.
type Token struct {
Raw string // Stringa Token originale, questo campo viene popolato quando si inizia ad analizzare
Method SigningMethod // Metodo utilizzato per la firma
Header map[string]interface{} // Parte header di JWT
Claims Claims // Parte payload di JWT
Signature string // Parte firma di JWT, questo campo viene popolato quando si inizia ad analizzare
Valid bool // JWT è valido
}La struttura Token rappresenta un Token JWT. L'uso dei campi dipende principalmente da come JWT viene creato/firmato o analizzato/verificato.
type RegisteredClaims struct {
// la claim `iss` (Issuer). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// la claim `sub` (Subject). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// la claim `aud` (Audience). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience ClaimStrings `json:"aud,omitempty"`
// la claim `exp` (Expiration Time). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt *NumericDate `json:"exp,omitempty"`
// la claim `nbf` (Not Before). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore *NumericDate `json:"nbf,omitempty"`
// la claim `iat` (Issued At). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt *NumericDate `json:"iat,omitempty"`
// la claim `jti` (JWT ID). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
ID string `json:"jti,omitempty"`
}Queste sono le Claims predefinite fornite dalla libreria, che possono essere utilizzate appropriatamente per raggiungere gli obiettivi desiderati.
Esempio 1. Creazione e Firma con HMAC
func TestHmac(t *testing.T) {
// Il tipo di chiave per hmac è un array di byte
secret := []byte("my secret")
// Utilizzo dell'algoritmo HS256, jwt.MapClaims è il payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// Firma
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Output:
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>Esempio 2. Utilizzo di Claims Predefinite
mySigningKey := []byte("AllYourBase")
// Creazione 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)Output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>Esempio 3. Claims Personalizzate
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// Creazione chiave
secret := []byte("my secret")
// Creazione 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",
},
}
// Creazione Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Firma
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
Quando si incorporano Claims standard in Claims personalizzate, è necessario assicurarsi che:
Le Claims standard incorporate siano di tipo non puntatore
Se sono di tipo puntatore, è meglio assicurarsi di allocare memoria appropriata prima di passarle, altrimenti si verificherà un panic.
Esempio 4. Analisi e Verifica Token HMAC
func TestParse(t *testing.T) {
secret := []byte("my secret")
// Supponiamo che un token sia stato creato e firmato con l'algoritmo HS256
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Passa la stringa del token e la funzione hook di verifica, il valore di ritorno è una struttura Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifica se l'algoritmo di firma corrisponde
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Algoritmo di firma non corrispondente: %s", token.Header["alg"])
}
// Restituisce la chiave di verifica
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)
}
}Output:
map[id:123456 name:jack]Esempio 5. Gestione Errori
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// Supponiamo che un token sia stato creato e firmato con l'algoritmo HS256
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Passa la stringa del token e la funzione hook di verifica, il valore di ritorno è una struttura Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifica se l'algoritmo di firma corrisponde
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Algoritmo di firma non corrispondente: %s", token.Header["alg"])
}
// Restituisce la chiave di verifica
return secret, nil
})
if token.Valid {
fmt.Println("token valido")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("La stringa passata non è nemmeno un token...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("Il token è scaduto o non è ancora valido")
} else {
fmt.Println("Elaborazione del token anomala...")
}
}Output:
token validoEsempio 6. Analisi Claims Personalizzate
Se si utilizzano Claims personalizzate durante la creazione del Token, durante l'analisi, se si desidera che le Claims possano essere convertite direttamente in Claims personalizzate invece di map, è necessario passare Claims personalizzate.
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"})) // Utilizzo option per la verifica
// Type assertion
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Output:
&{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 }}Esempio 7. Firma e Analisi RSA
RSA è comunemente utilizzato nell'architettura distribuita. Il processo approssimativo è il seguente:
- Il centro di autenticazione crea una coppia di chiavi, utilizza la chiave privata per firmare il jwt, il jwt viene restituito al client, la chiave pubblica è detenuta dal servizio business
- Il client invia una richiesta al servizio business con il jwt, il modulo business analizza il jwt utilizzando la chiave pubblica senza bisogno di accedere al centro di autenticazione
- Se l'autenticazione ha successo, restituisce le informazioni business
- Se l'autenticazione fallisce, restituisce informazioni di fallimento
func TestRsa(t *testing.T) {
// Creazione coppia di chiavi
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)
// Crittografia con chiave privata
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// Decrittografia con chiave pubblica
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)
}
}