I JSON Web Token (JWT) forniscono un modo sicuro per trasmettere claims tra le parti. Sono particolarmente utili nelle architetture a microservizi dove è necessario passare informazioni di autenticazione e autorizzazione tra servizi. I JWT sono token auto-contenuti che possono includere informazioni utente, permessi e altri claims.
Per una spiegazione dettagliata del flusso di autenticazione JWT, vedi la Guida al Flusso di Autenticazione JWT.
Un JWT è composto da tre parti (vedi JWT.io Debugger per esempi live):
Esempio JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Per maggiori informazioni sui claims JWT, vedi la Documentazione Claims JWT.
Gli scope sono permessi che determinano quali azioni un client può eseguire con un’API. Pensa agli scope come un modo per implementare il controllo degli accessi granulare. Per esempio:
read
per visualizzare i datiread
che write
backup
Ecco un’analogia del mondo reale:
room:access
- Accesso solo alla tua stanzapool:access
- Accesso alla piscinagym:access
- Accesso alla palestraall:access
- Accesso completo a tutte le struttureGli scope tipicamente seguono un pattern come risorsa:azione
. Esempi comuni:
api:read # Accesso in sola lettura all'API
api:write # Accesso in scrittura all'API
users:create # Capacità di creare utenti
admin:* # Accesso admin completo
Gli scope possono essere gerarchici. Per esempio:
api:read
, un token con admin:*
potrebbe essere validoEsempio di gerarchia scope:
admin:* # Accesso admin completo (include tutti gli scope admin)
├── admin:read # Lettura risorse admin
├── admin:write # Modifica risorse admin
└── admin:delete # Eliminazione risorse admin
Prima, definisci quali scope esistono nella tua API:
var JWTAuth = JWTSecurity("jwt", func() {
Description("Autenticazione JWT con scope")
// Definisci tutti gli scope disponibili
Scope("api:read", "Accesso in lettura alle risorse API")
Scope("api:write", "Accesso in scrittura alle risorse API")
Scope("api:admin", "Accesso amministrativo completo")
Scope("users:read", "Lettura profili utente")
Scope("users:write", "Modifica profili utente")
})
Poi, specifica quali scope sono richiesti per ogni endpoint:
var _ = Service("users", func() {
// Lista utenti - richiede accesso in lettura
Method("list", func() {
Security(JWTAuth, func() {
// Necessita solo accesso in lettura
Scope("users:read")
})
})
// Aggiorna utente - richiede accesso in scrittura
Method("update", func() {
Security(JWTAuth, func() {
// Necessita sia accesso in lettura che scrittura
Scope("users:read", "users:write")
})
})
// Elimina utente - richiede accesso admin
Method("delete", func() {
Security(JWTAuth, func() {
Scope("api:admin")
})
})
})
Quando generi token, includi gli scope concessi:
func GenerateUserToken(user *User) (string, error) {
// Determina gli scope basati sul ruolo utente
var scopes []string
switch user.Role {
case "admin":
scopes = []string{"api:admin", "users:read", "users:write"}
case "editor":
scopes = []string{"users:read", "users:write"}
default:
scopes = []string{"users:read"}
}
claims := Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
IssuedAt: time.Now().Unix(),
Subject: user.ID,
},
Scopes: scopes, // Includi scope nel token
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}
Quando elabori le richieste, valida che il token abbia gli scope richiesti:
func validateScopes(tokenScopes []string, requiredScopes []string) error {
// Crea una mappa degli scope del token per una ricerca efficiente
scopeMap := make(map[string]bool)
for _, scope := range tokenScopes {
scopeMap[scope] = true
}
// Caso speciale: scope admin concede tutto l'accesso
if scopeMap["api:admin"] {
return nil
}
// Controlla ogni scope richiesto
for _, required := range requiredScopes {
if !scopeMap[required] {
return fmt.Errorf("scope richiesto mancante: %s", required)
}
}
return nil
}
Convenzione di Denominazione
risorsa:azione
)Granularità
Documentazione
Sicurezza
Gestione
Prima, definisci il tuo schema di sicurezza JWT nel tuo pacchetto di design.
package design
import (
. "goa.design/goa/v3/dsl"
)
// JWTAuth definisce il nostro schema di sicurezza
var JWTAuth = JWTSecurity("jwt", func() {
Description("Autenticazione JWT")
// Definisci scope per l'autorizzazione
Scope("api:read", "Accesso in lettura all'API")
Scope("api:write", "Accesso in scrittura all'API")
})
L’autenticazione JWT può essere applicata a diversi livelli con requisiti di scope specifici:
// Livello API - si applica a tutti i servizi e metodi
var _ = API("secure_api", func() {
Security(JWTAuth)
})
// Livello Servizio - si applica a tutti i metodi nel servizio
var _ = Service("secure_service", func() {
Security(JWTAuth)
})
// Livello Metodo - si applica solo a questo metodo
Method("secure_method", func() {
Security(JWTAuth)
})
Per i metodi che usano l’autenticazione JWT, includi il token nel payload:
Method("getData", func() {
Security(JWTAuth, func() {
Scope("api:read") // Richiede scope di lettura
})
Payload(func() {
Token("token", String, "JWT per l'autenticazione")
Required("token")
// Campi payload aggiuntivi
Field(1, "query", String, "Query di ricerca")
})
Result(ArrayOf(String))
HTTP(func() {
GET("/data")
Param("query")
Response(StatusOK)
Response(StatusUnauthorized)
})
})
Quando Goa genera il codice, dovrai implementare un gestore di sicurezza:
// SecurityJWTFunc implementa la logica di autorizzazione per l'autenticazione JWT
func (s *service) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) {
claims, err := validateJWT(token)
if err != nil {
return ctx, err
}
// Valida gli scope
if err := validateScopes(claims.Scopes, scheme.RequiredScopes); err != nil {
return ctx, err
}
// Aggiungi claims al contesto
ctx = context.WithValue(ctx, "user_id", claims.Subject)
ctx = context.WithValue(ctx, "scopes", claims.Scopes)
return ctx, nil
}
func validateJWT(token string) (*Claims, error) {
// Implementa qui la tua logica di validazione JWT
// Questo dovrebbe:
// 1. Verificare la firma
// 2. Validare claims standard (exp, iat, ecc.)
// 3. Estrarre claims personalizzati
return claims, nil
}
Genera token sicuri e gestiscili correttamente:
func generateToken(claims jwt.Claims) (string, error) {
// Usa un algoritmo di firma sicuro
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
// Usa una chiave segreta forte
secret := os.Getenv("JWT_SECRET")
if len(secret) < 32 {
return "", fmt.Errorf("chiave segreta JWT troppo corta")
}
return token.SignedString([]byte(secret))
}
Implementa una validazione completa:
func validateToken(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifica metodo di firma
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("metodo di firma non valido: %v", token.Header["alg"])
}
// Verifica claims
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if err := claims.Valid(); err != nil {
return nil, err
}
// Verifica claims aggiuntivi come necessario
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
}
Gestisci gli errori di autenticazione in modo appropriato:
func handleAuthError(err error) error {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
return genservice.MakeUnauthorized(fmt.Errorf("token scaduto"))
case errors.Is(err, jwt.ErrTokenNotValidYet):
return genservice.MakeUnauthorized(fmt.Errorf("token non ancora valido"))
default:
return genservice.MakeUnauthorized(fmt.Errorf("token non valido"))
}
}
Implementa un sistema per la rotazione dei token:
func refreshToken(oldToken string) (string, error) {
// Valida il vecchio token
claims, err := validateJWT(oldToken)
if err != nil {
return "", err
}
// Crea nuovo token con claims aggiornati
newClaims := Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
IssuedAt: time.Now().Unix(),
Subject: claims.Subject,
},
Scopes: claims.Scopes,
}
return generateToken(newClaims)
}
Ecco un esempio completo che mostra come implementare l’autenticazione JWT in un servizio Goa:
package design
import (
. "goa.design/goa/v3/dsl"
)
var JWTAuth = JWTSecurity("jwt", func() {
Description("Autenticazione JWT con scope")
Scope("api:read", "Accesso in lettura")
Scope("api:write", "Accesso in scrittura")
})
var _ = API("auth_api", func() {
Title("API Autenticata")
Description("API che dimostra l'autenticazione JWT")
// Applica JWT di default
Security(JWTAuth)
})
var _ = Service("documents", func() {
Description("Servizio gestione documenti")
Method("list", func() {
Description("Lista documenti")
Security(JWTAuth, func() {
Scope("api:read")
})
Result(ArrayOf(Document))
HTTP(func() {
GET("/documents")
Response(StatusOK)
Response(StatusUnauthorized)
})
})
Method("create", func() {
Description("Crea documento")
Security(JWTAuth, func() {
Scope("api:write")
})
Payload(Document)
Result(Document)
HTTP(func() {
POST("/documents")
Response(StatusCreated)
Response(StatusUnauthorized)
})
})
})