Tarkibga o'tish

JWT middleware

Auth middleware yozish darsida net/http uchun JWT middleware yozilgan edi. Bu darsda xuddi shu mantiqni Gin’ga ko‘chiramiz. Tokenni tekshirish qoidalari o‘zgarmaydi. Faqat middleware’ning shakli, foydalanuvchini saqlash usuli va marshrutlarni himoyalash yo‘li o‘zgaradi.

Token yaratish va tekshirish uchun JWT yaratish va tekshirish darsidagi TokenService ishlatiladi. U HTTP haqida hech narsa bilmaydi, shuning uchun Gin bilan ham o‘zgarishsiz ishlaydi.

net/http va Gin middleware’lari farqi

net/http Gin
Imzo func(http.Handler) http.Handler func(*gin.Context)
Keyingisini chaqirish next.ServeHTTP(w, r) c.Next()
To‘xtatish nextni chaqirmaslik c.Abort() + return
Qiymat uzatish context.WithValue c.Set / c.Get

Middleware

const principalKey = "principal"

type Principal struct {
    UserID int64
    Role   string
}

func AuthRequired(tokens *TokenService) gin.HandlerFunc {
    return func(c *gin.Context) {
        raw, ok := bearerToken(c.GetHeader("Authorization"))
        if !ok {
            unauthorized(c, "missing_token", "Authorization sarlavhasida Bearer token kerak")
            return
        }

        claims, err := tokens.Parse(raw)
        switch {
        case errors.Is(err, jwt.ErrTokenExpired):
            unauthorized(c, "token_expired", "token muddati tugagan")
            return
        case err != nil:
            unauthorized(c, "invalid_token", "token yaroqsiz")
            return
        }

        userID, err := strconv.ParseInt(claims.Subject, 10, 64)
        if err != nil {
            unauthorized(c, "invalid_token", "token yaroqsiz")
            return
        }

        c.Set(principalKey, Principal{UserID: userID, Role: claims.Role})
        c.Next()
    }
}

func unauthorized(c *gin.Context, code, msg string) {
    c.Header("WWW-Authenticate", `Bearer error="invalid_token"`)
    c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
        "error": gin.H{"code": code, "message": msg},
    })
}
  • AuthRequired middleware qaytaradigan funksiya. TokenService unga parametr sifatida beriladi, global o‘zgaruvchi ishlatilmaydi.
  • c.AbortWithStatusJSON javobni yozadi va zanjirni to‘xtatadi. Undan keyingi return joriy funksiyani tugatadi.
  • Foydalanuvchi c.Set bilan saqlanadi.

Foydalanuvchini olish

c.Get any qaytaradi. Har bir handlerda tur tekshiruvini takrorlamaslik uchun yordamchi funksiya yozamiz:

func CurrentPrincipal(c *gin.Context) (Principal, bool) {
    v, ok := c.Get(principalKey)
    if !ok {
        return Principal{}, false
    }
    p, ok := v.(Principal)
    return p, ok
}

Kalit oddiy satr bo‘lgani uchun uni konstanta sifatida e’lon qildik. Shunda turli joylarda "principal" va "Principal" kabi har xil yozilib qolishining oldi olinadi.

Service qatlamiga uzatish

Service gin.Contextni bilmasligi kerak. Unga faqat kerakli qiymatlar va so‘rov contexti uzatiladi:

func (h *Handler) createBook(c *gin.Context) {
    p, _ := CurrentPrincipal(c)
    book, err := h.books.Create(c.Request.Context(), p.UserID, req.Title)
    // ...
}

Principal’ni context.Context orqali ham uzatish kerak bo‘lsa, middleware’da so‘rov contextini ham yangilash mumkin:

c.Request = c.Request.WithContext(withPrincipal(c.Request.Context(), p))

Bu usul net/http uchun yozilgan kod bilan birga ishlatilganda qulay.

Guruhlarni himoyalash

r := gin.New()
r.Use(gin.Logger(), gin.Recovery())

r.POST("/auth/login", h.login)
r.GET("/books", h.listBooks)

authed := r.Group("")
authed.Use(AuthRequired(tokens))
{
    authed.GET("/me", h.me)
    authed.POST("/books", RequirePermission("books:create"), h.createBook)

    admin := authed.Group("/admin", RequireRole("admin"))
    admin.GET("/stats", h.stats)
}
  • r.Group("") — prefikssiz guruh. U faqat middleware’larni birlashtiradi.
  • RequirePermission("books:create") faqat bitta marshrutga qo‘shildi. Gin’da marshrutga istalgancha handler berish mumkin: r.POST(yo‘l, mw1, mw2, handler).
  • authed.Group("/admin", RequireRole("admin")) — guruh yaratilayotganda middleware darhol qo‘shildi.

Warning

Gin arxitekturasi darsida aytilganidek, Use marshrutlardan oldin chaqirilishi kerak. authed.Use(...) qatori authed.GET(...) dan keyin yozilsa, /me endpointi himoyasiz qoladi.

Ruxsat middleware’lari

func RequireRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        p, ok := CurrentPrincipal(c)
        if !ok {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized"}})
            return
        }
        if !slices.Contains(roles, p.Role) {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": gin.H{"code": "forbidden"}})
            return
        }
        c.Next()
    }
}

RequirePermission ham xuddi shunday yoziladi. Faqat rol ro‘yxati o‘rniga RBAC darsidagi Can(role, perm) funksiyasi chaqiriladi.

To‘liq misol

main.go
package main

import (
    "errors"
    "log"
    "net/http"
    "os"
    "slices"
    "strconv"
    "strings"
    "time"

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

// ---------- Tokenlar ----------

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

type TokenService struct {
    secret []byte
    ttl    time.Duration
}

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

func (s *TokenService) Parse(raw string) (*Claims, error) {
    c := &Claims{}
    _, err := jwt.ParseWithClaims(raw, c,
        func(*jwt.Token) (any, error) { return s.secret, nil },
        jwt.WithValidMethods([]string{"HS256"}),
        jwt.WithIssuer("books-api"),
        jwt.WithExpirationRequired(),
    )
    return c, err
}

// ---------- Middleware'lar ----------

const principalKey = "principal"

type Principal struct {
    UserID int64
    Role   string
}

func CurrentPrincipal(c *gin.Context) (Principal, bool) {
    v, ok := c.Get(principalKey)
    if !ok {
        return Principal{}, false
    }
    p, ok := v.(Principal)
    return p, ok
}

func bearerToken(h string) (string, bool) {
    scheme, token, ok := strings.Cut(h, " ")
    if !ok || !strings.EqualFold(scheme, "Bearer") || token == "" {
        return "", false
    }
    return strings.TrimSpace(token), true
}

func unauthorized(c *gin.Context, code, msg string) {
    c.Header("WWW-Authenticate", `Bearer error="invalid_token"`)
    c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": code, "message": msg}})
}

func AuthRequired(tokens *TokenService) gin.HandlerFunc {
    return func(c *gin.Context) {
        raw, ok := bearerToken(c.GetHeader("Authorization"))
        if !ok {
            unauthorized(c, "missing_token", "Authorization sarlavhasida Bearer token kerak")
            return
        }

        claims, err := tokens.Parse(raw)
        switch {
        case errors.Is(err, jwt.ErrTokenExpired):
            unauthorized(c, "token_expired", "token muddati tugagan")
            return
        case err != nil:
            unauthorized(c, "invalid_token", "token yaroqsiz")
            return
        }

        userID, err := strconv.ParseInt(claims.Subject, 10, 64)
        if err != nil {
            unauthorized(c, "invalid_token", "token yaroqsiz")
            return
        }

        c.Set(principalKey, Principal{UserID: userID, Role: claims.Role})
        c.Next()
    }
}

func RequireRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        p, ok := CurrentPrincipal(c)
        if !ok {
            unauthorized(c, "unauthorized", "tizimga kiring")
            return
        }
        if !slices.Contains(roles, p.Role) {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
                "error": gin.H{"code": "forbidden", "message": "bu bo‘lim uchun ruxsat yo‘q"},
            })
            return
        }
        c.Next()
    }
}

// ---------- Handlerlar ----------

// Namuna uchun foydalanuvchilar. Real loyihada ular bazada, parollar esa bcrypt bilan saqlanadi.
var demoUsers = map[string]Principal{
    "aziza": {UserID: 1, Role: "author"},
    "admin": {UserID: 2, Role: "admin"},
}

type Handler struct {
    tokens *TokenService
}

func (h *Handler) login(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "bad_request"}})
        return
    }
    p, ok := demoUsers[req.Username]
    if !ok {
        unauthorized(c, "invalid_credentials", "login yoki parol noto‘g‘ri")
        return
    }
    token, err := h.tokens.Issue(p.UserID, p.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "internal"}})
        return
    }
    c.JSON(http.StatusOK, gin.H{"access_token": token, "token_type": "Bearer"})
}

func (h *Handler) me(c *gin.Context) {
    p, _ := CurrentPrincipal(c)
    c.JSON(http.StatusOK, gin.H{"data": gin.H{"id": p.UserID, "role": p.Role}})
}

func (h *Handler) stats(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"data": gin.H{"users": len(demoUsers)}})
}

func main() {
    secret := os.Getenv("JWT_SECRET")
    if len(secret) < 32 {
        log.Fatal("JWT_SECRET kamida 32 belgi bo‘lishi kerak")
    }
    tokens := &TokenService{secret: []byte(secret), ttl: 15 * time.Minute}
    h := &Handler{tokens: tokens}

    gin.SetMode(gin.ReleaseMode)
    r := gin.New()
    r.Use(gin.Logger(), gin.Recovery())

    r.POST("/auth/login", h.login)

    authed := r.Group("")
    authed.Use(AuthRequired(tokens))
    {
        authed.GET("/me", h.me)

        admin := authed.Group("/admin", RequireRole("admin"))
        admin.GET("/stats", h.stats)
    }

    r.Run(":8080")
}

Info

Misolda diqqat faqat middleware’ga qaratilishi uchun login soddalashtirilgan: parol tekshirilmaydi. Parolni to‘g‘ri tekshirish Login / Signup endpointlari darsida ko‘rsatilgan.

Sinab ko‘ramiz:

export JWT_SECRET=$(openssl rand -hex 32)
go run main.go
U=http://localhost:8080
AZIZA=$(curl -s -X POST $U/auth/login -d '{"username":"aziza"}' | jq -r .access_token)
ADMIN=$(curl -s -X POST $U/auth/login -d '{"username":"admin"}' | jq -r .access_token)

curl -s $U/me
curl -s $U/me -H "Authorization: Bearer $AZIZA"
curl -s $U/admin/stats -H "Authorization: Bearer $AZIZA"
curl -s $U/admin/stats -H "Authorization: Bearer $ADMIN"
{"error":{"code":"missing_token","message":"Authorization sarlavhasida Bearer token kerak"}}
{"data":{"id":1,"role":"author"}}
{"error":{"code":"forbidden","message":"bu bo‘lim uchun ruxsat yo‘q"}}
{"data":{"users":2}}

Tayyor kutubxonalar haqida

Gin uchun tayyor JWT middleware kutubxonalari ham bor. Masalan, github.com/appleboy/gin-jwt. Ular login, tokenni yangilash va chiqish endpointlarini ham tayyor holda beradi.

Tayyor kutubxona tanlaganda quyidagilarni tekshiring:

  • u qaysi JWT kutubxonasidan foydalanadi va u yangilanib turadimi;
  • algoritm qat’iy tekshiriladimi, ya’ni alg: none hujumidan himoyalanganmi;
  • xato javoblari formatini o‘zgartirish mumkinmi.

Bu darsdagi middleware atigi bir necha o‘n qatordan iborat. Uni o‘zingiz yozsangiz, har bir qator sizning nazoratingizda bo‘ladi va tashqi bog‘liqlik kamayadi.

Xulosa

  • Gin middleware’i func(*gin.Context) ko‘rinishida bo‘ladi va c.Next(), c.Abort() bilan boshqariladi.
  • Tokenni tekshirish mantiqi net/http versiyasi bilan bir xil. TokenService framework’dan mustaqil.
  • Foydalanuvchi c.Set bilan saqlanadi va yordamchi funksiya orqali o‘qiladi.
  • Himoyalangan marshrutlar guruhga yig‘iladi. Use marshrutlardan oldin chaqiriladi.
  • Ruxsat middleware’lari guruhga yoki alohida marshrutga qo‘shiladi.
  • Service qatlamiga gin.Context emas, c.Request.Context() va oddiy qiymatlar uzatiladi.

Keyingi darsda Gin’da xatolarni markazlashgan holda qayta ishlashni ko‘ramiz.