JWT
JWT modern sunucu iletişimi kimlik doğrulamasının ana akım yöntemlerinden biri haline gelmiştir. Hafif ve durumsuz özelliklere sahiptir.
Giriş
JWT tam adı JSON Web Tokens olan JWT hakkında detaylı bilgi [jwt.io](JSON Web Token Introduction - jwt.io) adresinde bulunabilir. Açık güvenli kompakt JSON objesi taşıyıcı olarak hizmetler arasında bilgi taşıma yöntemidir. Özellikleri yüksek güvenlik içeriğin değiştirilmesini önleme ve düşük tüketimdir.
Yapı
RFC standardına göre JWT aşağıdaki üç bölümden oluşur:
- Header (Başlık)
- Payload (Yük)
- Signature (İmza)
Her bölüm bir nokta . ile ayrılır ve header.payload.signature formatında bir string oluşturur. Bu JWT token'ının standart yapısıdır. Her bir yapının işlevi aşağıda açıklanmıştır.
TIP
base64 ile base64URL aynı kodlama yöntemi değildir. İkincisi web URL'leri ile uyumludur ve bunlar için escape yapar.
Başlık
Başlık sadece bazı temel bilgileri beyan eder. Genellikle iki bölümden oluşur: token tipi ve imza için kullanılan şifreleme algoritması. Örneğin:
{
"alg": "HS256",
"typ": "JWT"
}Yukarıdaki bilgiler token tipinin JWT olduğunu imza bölümünde kullanılan şifreleme algoritmasının HS256 olduğunu gösterir. JSON objesi Base64Url ile kodlanarak string'e dönüştürülür. Bu string JWT'nin başlık bölümüdür.
Yük
JWT'nin ikinci bölümü yük bölümüdür. Temel olarak beyanlar (claims) içerir. Beyanlar genellikle bir varlık hakkında veriler içerir. Örneğin bir kullanıcı. Toplam üç tür beyan tipi vardır:
registered:Registered claimsönceden tanımlanmış beyanlardır. Kullanımı zorunlu değildir ancak önerilir. Örneğin:iss(issuer - düzenleyen),exp(expiration time - son kullanma tarihi),aud(audience - hedef kitle).public:Public claimsJWT kullananlar tarafından serbestçe tanımlanabilir. Diğer beyanlarla çakışmaktan kaçınılmalıdır.private claims: Bu bölümdeki beyanlar da özelleştirilmiştir. Genellikle hizmetler arasında bilgi paylaşımı için kullanılır.
Örnek bir yük:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}Bu JSON objesi Base64Url ile kodlanarak string'e dönüştürülür ve JWT'nin ikinci bölümünü oluşturur.
DANGER
Yük bölümü korunmuş olsa ve değiştirilmezlik özelliği olsa da bu bölüm herkese açık olarak okunabilir. Bu nedenle JWT içinde hassas bilgiler bulundurmayın.
İmza
Kodlanmış başlık ve yük bölümleri elde edildikten sonra başlıkta belirtilen imza algoritması kullanılarak bu iki bölümün içeriği ve anahtar ile şifrelenerek imza oluşturulur. Bu nedenle JWT içeriğinde herhangi bir değişiklik olduğunda şifre çözme sırasında elde edilen imza farklı olacaktır. Ayrıca özel anahtar kullanılıyorsa JWT'nin düzenleyicisi de doğrulanabilir.
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)Örneğin:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)Sonuç olarak üç base64Url string'inden oluşan ve . ile ayrılan bir string elde edilir. Aşağıdaki gibi görünür:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQÇalışma Prensibi
Kimlik doğrulamada kullanıcı kimlik bilgileri ile başarılı bir şekilde giriş yaptığında bir JSON Web token döndürülür. Token bir kimlik bilgisi olduğu için güvenlik sorunlarını önlemek için çok dikkatli olunmalıdır. Genel olarak token gerekli olandan daha uzun süre saklanmamalıdır. Kullanıcı korumalı rotalara ve kaynaklara erişmek istediğinde istek yaparken token taşımalıdır. Genellikle istek başlığında Authorization header'ında Bearer schema kullanılır. Örneğin:
Authorization: Bearer <token>Sunucu JWT'yi aldıktan sonra geçerlilik doğrulaması yapar. İçerik değiştirilmiş mi token süresi dolmuş mu vb. Doğrulama geçerse kaynaklara erişilebilir. JWT bazı temel bilgileri taşıyabilse de bilginin çok büyük olmaması önerilir.
JWT Kütüphanesi
Resmi Depo: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
Resmi Dokümantasyon: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
Bu kütüphane JWT ayrıştırma doğrulama oluşturma ve imzalama işlemlerini destekler. Şu anda desteklenen imza algoritmaları HMAC SHA, RSA, RSA-PSS ve ECDSA'dır. Ancak kendi hook'larınızı da ekleyebilirsiniz.
Kurulum
go get -u github.com/golang-jwt/jwt/v4İçe Aktarma
import "github.com/golang-jwt/jwt/v4"İmza Algoritması Seçimi
Birkaç imza algoritması mevcuttur. Kullanmadan önce aralarındaki farkları anlamak ve daha iyi bir seçim yapmak için algoritmaları incelemek gerekir. En büyük farkları simetrik şifreleme ve asimetrik şifreleme arasındadır.
En basit simetrik şifreleme algoritması HSA herhangi bir []byte değerinin geçerli anahtar olarak kullanılmasını sağlar. Bu nedenle hesaplama hızı biraz daha hızlıdır. Üretici ve tüketici tarafları güvenildiğinde simetrik şifreleme algoritmaları en verimli olanıdır. Ancak imzalama ve doğrulama için aynı anahtar kullanıldığından doğrulama için anahtarın dağıtımı kolay değildir. İmza anahtarı da aynı olduğundan imza sızdırıldığında JWT güvenliği anlamsız hale gelir.
RSA gibi asimetrik şifreleme imzalama yöntemleri token'ı imzalamak ve doğrulamak için farklı anahtarlar kullanır. Bu özel anahtar ile token oluşturmayı ve genel anahtar kullanan herhangi birinin doğrulama yapmasını mümkün kılar.
Farklı imza algoritmaları farklı anahtar türleri gerektirir. Aşağıda bazı yaygın imza algoritmalarının türleri verilmiştir:
HMAC: Simetrik şifreleme. İmzalama ve doğrulama için[]bytetüründe değer gerekir. (HS256,HS384,HS512)RSA: Asimetrik şifreleme. İmzalama için*rsa.PrivateKeytüründe değer ve doğrulama için*rsa.PublicKeytüründe değer gerekir. (RS256,RS384,RS512)ECDSA: Asimetrik şifreleme. İmzalama için*ecdsa.PrivateKeytüründe değer ve doğrulama için*ecdsa.PublicKeytüründe değer gerekir. (ES256,ES384,ES512)EdDSA: Asimetrik şifreleme. İmzalama içined25519.PrivateKeytüründe değer ve doğrulama içined25519.PublicKeytüründe değer gerekir. (Ed25519)
Örnekler
Aşağıda JWT oluşturma ve imzalama ile ayrıştırma ve doğrulama ile ilgili bazı örnekler gösterilecektir.
type Token struct {
Raw string // Orijinal token string'i. Ayrıştırma başladığında bu alan doldurulur
Method SigningMethod // İmzalama için kullanılan yöntem
Header map[string]interface{} // JWT başlık bölümü
Claims Claims // JWT yük bölümü
Signature string // JWT imza bölümü. Ayrıştırma başladığında bu alan doldurulur
Valid bool // JWT geçerli mi
}Token struct'ı bir JWT Token'ı temsil eder. Alanların kullanımı JWT'nin nasıl oluşturulduğuna/imzalandığına veya ayrıştırıldığına/doğrulandığına bağlıdır.
type RegisteredClaims struct {
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience ClaimStrings `json:"aud,omitempty"`
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt *NumericDate `json:"exp,omitempty"`
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore *NumericDate `json:"nbf,omitempty"`
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt *NumericDate `json:"iat,omitempty"`
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
ID string `json:"jti,omitempty"`
}Bu kütüphane tarafından sağlanan önceden tanımlanmış Claims'tır. İhtiyaçlara uygun olarak kullanılabilir.
Örnek 1. HMAC Oluşturma ve İmzalama
func TestHmac(t *testing.T) {
// hmac anahtar türü byte dizisidir
secret := []byte("my secret")
// HS256 algoritması kullanılır. jwt.MapClaims payload'dır
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// İmzala
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Çıktı:
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>Örnek 2. Önceden Tanımlanmış Claims Kullanımı
mySigningKey := []byte("AllYourBase")
// Claims oluştur
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)Çıktı:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>Örnek 3. Özel Claims
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// Anahtar oluştur
secret := []byte("my secret")
// Claims oluştur
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 oluştur
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// İmzala
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Çıktı:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
Özel Claims içinde standart Claims gömüldüğünde şunlardan emin olun:
Gömülen standart Claims pointer olmayan türde olmalıdır.
Eğer pointer türündeyse geçirmeden önce uygun belleği ayırdığınızdan emin olun. Aksi takdirde panic oluşur.
Örnek 4. HMAC ile Token Ayrıştırma ve Doğrulama
func TestParse(t *testing.T) {
secret := []byte("my secret")
// HS256 algoritması ile oluşturulmuş ve imzalanmış bir token olduğunu varsayalım
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Token string'i ve doğrulama hook fonksiyonunu iletin. Dönüş değeri Token struct'ıdır
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// İmza algoritmasının eşleşip eşleşmediğini doğrula
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Eşleşmeyen imza algoritması: %s", token.Header["alg"])
}
// Doğrulama anahtarını döndür
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)
}
}Çıktı:
map[id:123456 name:jack]Örnek 5. Hata Yönetimi
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// HS256 algoritması ile oluşturulmuş ve imzalanmış bir token olduğunu varsayalım
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Token string'i ve doğrulama hook fonksiyonunu iletin. Dönüş değeri Token struct'ıdır
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// İmza algoritmasının eşleşip eşleşmediğini doğrula
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Eşleşmeyen imza algoritması: %s", token.Header["alg"])
}
// Doğrulama anahtarını döndür
return secret, nil
})
if token.Valid {
fmt.Println("token geçerli")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("İletilen string bir token bile değil...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("Token süresi dolmuş veya henüz geçerli değil")
} else {
fmt.Println("Token işleme hatası...")
}
}Çıktı:
token geçerliÖrnek 6. Özel Claims Ayrıştırma
Token oluştururken özel Claims kullanıldıysa ayrıştırma sırasında Claims'ın map yerine özel Claims'a dönüştürülmesini istiyorsanız özel Claims iletmeniz gerekir.
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"})) // Doğrulama için option kullan
// Tür dönüşümü
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Çıktı:
&{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 }}Örnek 7. RSA İmzalama ve Ayrıştırma
RSA dağıtık mimarilerde daha yaygın kullanılır. Süreç aşağıdaki gibidir:
- Kimlik doğrulama merkezi anahtar çifti oluşturur. JWT'yi imzalamak için özel anahtarı kullanır. JWT istemciye döndürülür. Genel anahtar iş hizmetleri tarafından tutulur.
- İstemci JWT ile iş hizmetine istek yapar. İş hizmeti JWT'yi ayrıştırmak için genel anahtarı kullanır. Kimlik doğrulama merkezine erişmesine gerek yoktur.
- Kimlik doğrulama başarılıysa iş bilgisi döndürülür.
- Kimlik doğrulama başarısızsa hata bilgisi döndürülür.
func TestRsa(t *testing.T) {
// Anahtar çifti oluştur
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)
// Özel anahtar ile şifrele
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// Genel anahtar ile çöz
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)
}
}