Tarkibga o'tish

JWT yaratish va tekshirish

JWT (JSON Web Token) — ikki tomon o‘rtasida ma’lumotni xavfsiz uzatish uchun ishlatiladigan ixcham token formati. API’larda u ko‘pincha access token sifatida qo‘llanadi: login’dan keyin server JWT beradi, klient esa uni har bir so‘rovda Authorization: Bearer sarlavhasida qaytaradi.

Session va Token-based auth farqi darsida token imzolangan guvohnomaga o‘xshatilgan edi. Bu darsda o‘sha guvohnoma ichida nima yozilganini, imzo qanday qo‘yilishini va qanday tekshirilishini ko‘ramiz.

JWT tuzilishi

JWT nuqta bilan ajratilgan uchta qismdan iborat:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBeml6YSIsImV4cCI6MTc1OTIzMjgwMH0.4Hb6nX0cQ2kS7tZqL8mR1vYwJ3fE9pA5oU2iT6gD0hM
└──────────────┬─────────────────────┘ └──────────────────────────┬──────────────────────────────┘ └──────────────────┬───────────────────────┘
            header                                             payload                                          signature

Header — token turi va imzo algoritmi:

{"alg": "HS256", "typ": "JWT"}

Payload — token ichidagi ma’lumotlar, ya’ni claim'lar:

{"sub": "42", "name": "Aziza", "exp": 1759232800}

Signature — header va payload’dan maxfiy kalit yordamida hisoblangan imzo:

HMAC-SHA256(base64url(header) + "." + base64url(payload), maxfiy_kalit)

Header va payload Base64URL bilan kodlangan. Bu shifrlash emas. Istalgan odam tokenni ochib, ichini o‘qiy oladi:

echo 'eyJzdWIiOiI0MiIsIm5hbWUiOiJBeml6YSIsImV4cCI6MTc1OTIzMjgwMH0' | base64 -d
{"sub":"42","name":"Aziza","exp":1759232800}

Danger

JWT payload’iga parol, karta raqami, pasport ma’lumoti yoki boshqa maxfiy narsa yozmang. Token brauzer xotirasida, loglarda va proxy serverlarda qolishi mumkin. Imzo faqat ma’lumotni o‘zgartirishdan himoya qiladi, uni o‘qishdan emas.

Imzo nimani kafolatlaydi?

Hujumchi payload’dagi "sub":"42" qiymatini "sub":"1"ga, ya’ni administrator ID’siga almashtirib ko‘rdi deylik. Payload o‘zgargani uchun imzo endi unga mos kelmaydi. Yangi imzo yaratish uchun esa maxfiy kalit kerak, u faqat serverda bor. Server imzoni tekshirib, soxta tokenni rad etadi.

Standart claim’lar

JWT standarti (RFC 7519) bir nechta claim nomlarini belgilaydi:

Claim Nomi Ma’nosi
sub Subject Token kimga tegishli, odatda foydalanuvchi ID’si
exp Expiration Time Qachon muddati tugaydi (Unix vaqt, soniyalarda)
iat Issued At Qachon yaratilgan
nbf Not Before Qachondan boshlab haqiqiy
iss Issuer Kim yaratgan, masalan books-api
aud Audience Kim uchun mo‘ljallangan
jti JWT ID Tokenning noyob ID’si. Bekor qilish uchun kerak

Standart claim’lardan tashqari o‘z claim’laringizni ham qo‘shish mumkin, masalan role. Lekin tokenni katta qilib yubormang, u har bir so‘rovda yuboriladi.

Algoritm tanlash

Algoritm Turi Qachon
HS256 Simmetrik (HMAC): bitta maxfiy kalit Token yaratadigan va tekshiradigan servis bitta bo‘lsa
RS256, ES256, EdDSA Asimmetrik: yopiq kalit bilan imzolanadi, ochiq kalit bilan tekshiriladi Token bir servisda yaratilib, ko‘p servislarda tekshirilsa

Bitta API uchun HS256 yetarli va sodda. Asimmetrik algoritmda boshqa servislar tokenni ochiq kalit bilan tekshira oladi, lekin yangi token yarata olmaydi. Shuning uchun mikroservislarda ular afzal.

HS256 uchun kalit uzun va tasodifiy bo‘lishi kerak. Kamida 32 bayt:

openssl rand -hex 32

golang-jwt kutubxonasi

Go’da JWT bilan ishlash uchun keng qo‘llanadigan kutubxona — github.com/golang-jwt/jwt:

go get github.com/golang-jwt/jwt/v5

Claim’lar turi

type Claims struct {
    Role string `json:"role"`
    jwt.RegisteredClaims
}

jwt.RegisteredClaims standart claim’larni (sub, exp, iat va boshqalar) o‘z ichiga oladi. U struct ichiga nomsiz joylashtirilgan, shuning uchun uning maydonlari to‘g‘ridan-to‘g‘ri Claims darajasida ko‘rinadi. Role esa bizning qo‘shimcha claim’imiz.

Token yaratish

func (s *TokenService) Issue(userID int64, role string) (string, error) {
    now := s.now()
    claims := Claims{
        Role: role,
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   strconv.FormatInt(userID, 10),
            Issuer:    s.issuer,
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(s.ttl)),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(s.secret)
}

jwt.NewWithClaims header va payload’ni tayyorlaydi. SignedString ularni kodlaydi, imzolaydi va tayyor satrni qaytaradi. HS256 uchun kalit []byte ko‘rinishida beriladi.

Tokenni tekshirish

func (s *TokenService) Parse(tokenStr string) (*Claims, error) {
    claims := &Claims{}

    _, err := jwt.ParseWithClaims(tokenStr, claims,
        func(t *jwt.Token) (any, error) { return s.secret, nil },
        jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
        jwt.WithIssuer(s.issuer),
        jwt.WithExpirationRequired(),
        jwt.WithLeeway(30*time.Second),
        jwt.WithTimeFunc(s.now),
    )
    if err != nil {
        return nil, err
    }
    return claims, nil
}

ParseWithClaims uch ishni bajaradi:

  1. Tokenni qismlarga ajratadi va JSON’ni claimsga o‘qiydi.
  2. Uchinchi argumentdagi funksiya (Keyfunc) qaytargan kalit bilan imzoni tekshiradi.
  3. exp, iss va boshqa claim’larni opsiyalarga ko‘ra tekshiradi.

Har bir opsiya nima uchun kerak:

  • WithValidMethods — eng muhimi. Tokenning header qismida algoritm yozilgan va uni hujumchi o‘zgartirishi mumkin. Ma’lum hujumlardan birida "alg": "none" yoziladi, ya’ni "imzo yo‘q". Boshqasida esa asimmetrik va simmetrik algoritmlar almashtiriladi. Faqat kutilgan algoritmga ruxsat berilsa, bu hujumlar ishlamaydi.
  • WithExpirationRequired — exp claim’i bo‘lmagan tokenlarni rad etadi. Aks holda muddatsiz token cheksiz ishlaydi.
  • WithIssuer — token aynan bizning servis tomonidan yaratilganini tekshiradi. Bir xil kalitni ishlatgan boshqa servisning tokeni qabul qilinmaydi.
  • WithLeeway — serverlar soatlari orasidagi kichik farqni hisobga oladi.
  • WithTimeFunc — joriy vaqtni beradigan funksiya. Testlarda vaqtni boshqarish uchun qulay.

Xatolarni ajratish

golang-jwt xato turlarini errors.Is bilan tekshirish imkonini beradi:

switch {
case errors.Is(err, jwt.ErrTokenExpired):
    // muddati tugagan → klient tokenni yangilashi kerak
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
    // imzo noto‘g‘ri → soxta token
case errors.Is(err, jwt.ErrTokenMalformed):
    // token formati buzilgan
}

Klientga barcha holatlarda 401 qaytariladi. Lekin muddati tugagan token haqida alohida kod bilan xabar berish foydali: klient buni ko‘rib, refresh token orqali yangi token so‘raydi.

To‘liq misol

main.go
package main

import (
    "errors"
    "fmt"
    "log"
    "strconv"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    Role string `json:"role"`
    jwt.RegisteredClaims
}

type TokenService struct {
    secret []byte
    issuer string
    ttl    time.Duration
    now    func() time.Time
}

func NewTokenService(secret string, ttl time.Duration) (*TokenService, error) {
    if len(secret) < 32 {
        return nil, errors.New("JWT kaliti kamida 32 bayt bo‘lishi kerak")
    }
    return &TokenService{
        secret: []byte(secret),
        issuer: "books-api",
        ttl:    ttl,
        now:    time.Now,
    }, nil
}

func (s *TokenService) Issue(userID int64, role string) (string, error) {
    now := s.now()
    claims := Claims{
        Role: role,
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   strconv.FormatInt(userID, 10),
            Issuer:    s.issuer,
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(s.ttl)),
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.secret)
}

func (s *TokenService) Parse(tokenStr string) (*Claims, error) {
    claims := &Claims{}
    _, err := jwt.ParseWithClaims(tokenStr, claims,
        func(t *jwt.Token) (any, error) { return s.secret, nil },
        jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
        jwt.WithIssuer(s.issuer),
        jwt.WithExpirationRequired(),
        jwt.WithLeeway(30*time.Second),
        jwt.WithTimeFunc(s.now),
    )
    if err != nil {
        return nil, err
    }
    return claims, nil
}

func describe(err error) string {
    switch {
    case err == nil:
        return "token yaroqli"
    case errors.Is(err, jwt.ErrTokenExpired):
        return "token muddati tugagan"
    case errors.Is(err, jwt.ErrTokenSignatureInvalid):
        return "imzo noto‘g‘ri"
    case errors.Is(err, jwt.ErrTokenMalformed):
        return "token formati buzilgan"
    default:
        return "token yaroqsiz: " + err.Error()
    }
}

func main() {
    tokens, err := NewTokenService("8f3c1a9e7b2d4f6a0c5e9b1d3f7a2c4e", 15*time.Minute)
    if err != nil {
        log.Fatal(err)
    }

    // 1. Token yaratish
    tok, err := tokens.Issue(42, "reader")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Token:", tok[:40]+"...")

    // 2. Tekshirish
    claims, err := tokens.Parse(tok)
    fmt.Printf("1) %s: sub=%s role=%s\n", describe(err), claims.Subject, claims.Role)

    // 3. Payload'ni almashtirishga urinish
    parts := strings.Split(tok, ".")
    fake := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
        Role:             "admin",
        RegisteredClaims: jwt.RegisteredClaims{Subject: "1", Issuer: "books-api"},
    })
    fakeStr, _ := fake.SigningString() // imzosiz header.payload
    _, err = tokens.Parse(fakeStr + "." + parts[2])
    fmt.Println("2)", describe(err))

    // 4. Boshqa kalit bilan imzolangan token
    other, _ := NewTokenService("boshqa-kalit-boshqa-kalit-boshqa-kalit", time.Hour)
    otherTok, _ := other.Issue(1, "admin")
    _, err = tokens.Parse(otherTok)
    fmt.Println("3)", describe(err))

    // 5. "alg": "none" hujumi
    none := jwt.NewWithClaims(jwt.SigningMethodNone, Claims{Role: "admin"})
    noneTok, _ := none.SignedString(jwt.UnsafeAllowNoneSignatureType)
    _, err = tokens.Parse(noneTok)
    fmt.Println("4)", describe(err))

    // 6. Muddati tugagan token: soatni 20 daqiqa oldinga suramiz
    tokens.now = func() time.Time { return time.Now().Add(20 * time.Minute) }
    _, err = tokens.Parse(tok)
    fmt.Println("5)", describe(err))

    // 7. Buzilgan satr
    _, err = tokens.Parse("bu-token-emas")
    fmt.Println("6)", describe(err))
}
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
1) token yaroqli: sub=42 role=reader
2) imzo noto‘g‘ri
3) imzo noto‘g‘ri
4) imzo noto‘g‘ri
5) token muddati tugagan
6) token formati buzilgan

Har bir hujum rad etildi:

  • 2-holat. Payload role: admin qilib o‘zgartirildi, lekin imzo eski qoldi. Imzo mos kelmadi.
  • 3-holat. Token to‘g‘ri tuzilgan, lekin boshqa kalit bilan imzolangan.
  • 4-holat. Imzosiz (alg: none) token WithValidMethods tufayli rad etildi. Kutubxona ruxsat etilmagan algoritmni ham imzo xatosi sifatida qaytaradi.
  • 5-holat. Token 15 daqiqa yashaydi, soat esa 20 daqiqa oldinga surildi.

Login’ga token qo‘shish

Oldingi darsdagi login handleri endi token qaytaradi:

func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
    // ... email va parol tekshiruvi

    token, err := h.tokens.Issue(user.ID, user.Role)
    if err != nil {
        log.Printf("token yaratish: %v", err)
        writeError(w, http.StatusInternalServerError, "internal", "ichki server xatosi")
        return
    }

    w.Header().Set("Cache-Control", "no-store")
    writeJSON(w, http.StatusOK, map[string]any{
        "access_token": token,
        "token_type":   "Bearer",
        "expires_in":   int(h.tokens.ttl.Seconds()),
        "user":         toResponse(user),
    })
}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicmVhZGVyIiwiaXNzIjoiYm9va3MtYXBpIiwic3ViIjoiMSIsImV4cCI6MTc1OTIyNzMwMCwiaWF0IjoxNzU5MjI2NDAwfQ.9m1c...",
  "token_type": "Bearer",
  "expires_in": 900,
  "user": {"id": 1, "email": "[email protected]", "name": "Aziza", "role": "reader", "created_at": "..."}
}
  • token_type va expires_in maydonlari OAuth 2.0 standartidagi javob formatiga mos keladi. Klient kutubxonalari bu formatni yaxshi tushunadi.
  • Cache-Control: no-store token brauzer yoki proxy keshida saqlanib qolmasligini ta’minlaydi.

Access tokenning muddati

Muddat qanchalik qisqa bo‘lsa, o‘g‘irlangan token shunchalik kam vaqt ishlaydi. Lekin juda qisqa muddat foydalanuvchini tez-tez qayta kiritishga majbur qiladi. Amaliyotda:

  • access token — 5 daqiqadan 1 soatgacha, odatda 15 daqiqa;
  • refresh token — bir necha kundan bir necha haftagacha. U serverda saqlanadi va bekor qilinishi mumkin.

Refresh token oqimi bu kursda to‘liq yozilmaydi, lekin uning mantiqi Session va Token-based auth farqi darsida tushuntirilgan.

Xulosa

  • JWT uch qismdan iborat: header, payload va imzo. Header va payload shifrlanmagan, faqat kodlangan.
  • Payload’ga maxfiy ma’lumot yozilmaydi.
  • Imzo tokenni o‘zgartirishdan himoya qiladi. HS256 uchun kamida 32 baytlik tasodifiy kalit kerak.
  • Tekshirishda WithValidMethods bilan algoritmni qat’iy belgilang va exp claim’ini majburiy qiling.
  • Xato turlari errors.Is bilan ajratiladi. Muddati tugagan token alohida qayta ishlanadi.
  • Access token qisqa muddatli bo‘ladi.

Keyingi darsda tokenni har bir so‘rovda tekshiradigan auth middleware yozamiz va foydalanuvchini contextga joylaymiz.