Skip to content

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:

json
{
  "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 claims JWT 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:

json
{
  "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

go
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 []byte türünde değer gerekir. (HS256,HS384,HS512)
  • RSA: Asimetrik şifreleme. İmzalama için *rsa.PrivateKey türünde değer ve doğrulama için *rsa.PublicKey türünde değer gerekir. (RS256,RS384,RS512)
  • ECDSA: Asimetrik şifreleme. İmzalama için *ecdsa.PrivateKey türünde değer ve doğrulama için *ecdsa.PublicKey türünde değer gerekir. (ES256,ES384,ES512)
  • EdDSA: Asimetrik şifreleme. İmzalama için ed25519.PrivateKey türünde değer ve doğrulama için ed25519.PublicKey tü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.

go
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.

go
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

go
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ı

go
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

go
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:

  1. Gömülen standart Claims pointer olmayan türde olmalıdır.

  2. 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

go
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

go
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.

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"})) // 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:

  1. 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.
  2. İ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.
  3. Kimlik doğrulama başarılıysa iş bilgisi döndürülür.
  4. Kimlik doğrulama başarısızsa hata bilgisi döndürülür.
go
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)
   }
}

Golang by www.golangdev.cn edit