Tarkibga o'tish

Auth middleware yozish

Endi bizda foydalanuvchini ro‘yxatdan o‘tkazish, login qilish va JWT berish imkoniyati bor. Oxirgi qadam — himoyalangan endpointlarga kelgan har bir so‘rovda tokenni tekshirish.

Tekshiruvni har bir handlerga yozib chiqish mumkin, lekin bu xato. Bitta handlerda unutilsa, o‘sha endpoint hammaga ochiq qoladi. Shuning uchun tekshiruv middleware'ga chiqariladi. Middleware haqida Middleware va logging asoslari darsida gaplashgan edik.

Auth middleware quyidagilarni bajaradi:

  1. Authorization sarlavhasidan tokenni oladi.
  2. Tokenni tekshiradi.
  3. Token yaroqsiz bo‘lsa, 401 qaytaradi va so‘rovni to‘xtatadi.
  4. Token yaroqli bo‘lsa, foydalanuvchi ma’lumotini contextga joylaydi va so‘rovni handlerga uzatadi.

Tokenni sarlavhadan olish

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

HTTP standartiga ko‘ra sxema nomi (Bearer) harf katta-kichikligiga bog‘liq emas. Shu sababli bearer eyJ... ham qabul qilinadi.

Foydalanuvchini contextga joylash

Middleware tokenni tekshirgach, foydalanuvchi kimligini handlerga yetkazishi kerak. Buning uchun so‘rov contexti ishlatiladi:

type Principal struct {
    UserID int64
    Role   string
}

type principalKey struct{}

func withPrincipal(ctx context.Context, p Principal) context.Context {
    return context.WithValue(ctx, principalKey{}, p)
}

func PrincipalFrom(ctx context.Context) (Principal, bool) {
    p, ok := ctx.Value(principalKey{}).(Principal)
    return p, ok
}

Principal — xavfsizlik terminologiyasida tizimda amal bajarayotgan subyekt. U foydalanuvchi, servis yoki boshqa narsa bo‘lishi mumkin.

  • Kalit sifatida eksport qilinmagan principalKey turi ishlatildi. Boshqa paket xuddi shu kalit bilan qiymatni almashtira olmaydi.
  • Contextdan o‘qish faqat PrincipalFrom funksiyasi orqali bo‘ladi. Handlerlar kalit turini bilishi shart emas.
  • Contextga butun User obyekti emas, faqat kerakli minimal ma’lumot joylanadi.

Middleware

func (a *Auth) Require(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        raw, ok := bearerToken(r)
        if !ok {
            unauthorized(w, "missing_token", "Authorization sarlavhasida Bearer token kerak")
            return
        }

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

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

        ctx := withPrincipal(r.Context(), Principal{UserID: userID, Role: claims.Role})
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func unauthorized(w http.ResponseWriter, code, msg string) {
    w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
    writeError(w, http.StatusUnauthorized, code, msg)
}

Muhim jihatlar:

  • Har bir xato holatidan keyin return bor. next.ServeHTTP faqat token to‘liq tekshirilgandan keyin chaqiriladi.
  • Muddati tugagan token uchun alohida token_expired kodi qaytariladi. Klient uni ko‘rib, tokenni yangilaydi yoki foydalanuvchini login sahifasiga yuboradi.
  • Qolgan xatolarning tafsiloti, masalan imzo noto‘g‘riligi yoki format buzilgani, klientga aytilmaydi. Bu ma’lumot hujumchiga foyda keltirishi mumkin.
  • WWW-Authenticate sarlavhasi HTTP standartida 401 javobi uchun talab qilinadi.

Himoyalangan va ochiq endpointlar

Go 1.22’dagi ServeMux bilan middleware’ni alohida endpointlarga qo‘llash qulay:

mux := http.NewServeMux()

// Ochiq endpointlar
mux.HandleFunc("POST /auth/signup", h.signup)
mux.HandleFunc("POST /auth/login", h.login)
mux.HandleFunc("GET /books", h.listBooks)

// Himoyalangan endpointlar
mux.Handle("GET /me", auth.Require(http.HandlerFunc(h.me)))
mux.Handle("POST /books", auth.Require(http.HandlerFunc(h.createBook)))

GET /books hammaga ochiq, POST /books esa faqat tizimga kirganlar uchun. Metodli patternlar tufayli bitta yo‘lning turli metodlariga turlicha himoya qo‘yish oson.

Tip

Endpointlar ko‘payganda himoyani har biriga qo‘lda qo‘shish o‘rniga, ularni guruhlash qulay. Masalan, barcha himoyalangan endpointlarni alohida mux’ga yig‘ib, butun mux’ni bitta Require bilan o‘rash mumkin. gorilla/mux va Gin kabi routerlarda buning uchun tayyor guruhlash imkoniyati bor.

Handlerda foydalanuvchini olish

func (h *Handler) me(w http.ResponseWriter, r *http.Request) {
    p, ok := PrincipalFrom(r.Context())
    if !ok {
        // Bu holat faqat middleware ulanmay qolgan bo‘lsa yuz beradi
        writeError(w, http.StatusInternalServerError, "internal", "ichki server xatosi")
        return
    }

    user, err := h.users.ByID(r.Context(), p.UserID)
    if err != nil {
        unauthorized(w, "invalid_token", "foydalanuvchi topilmadi")
        return
    }

    writeJSON(w, http.StatusOK, map[string]any{"data": toResponse(user)})
}

ok == false holati dasturchi xatosini bildiradi: handler middleware’siz ro‘yxatdan o‘tkazilgan. Bu holatda 500 qaytariladi va xato darhol ko‘zga tashlanadi.

Foydalanuvchi bazadan qayta o‘qilishiga ham e’tibor bering. Token berilgandan keyin akkaunt o‘chirilgan yoki bloklangan bo‘lishi mumkin. Muhim amallarda tokendagi ma’lumotga ko‘r-ko‘rona ishonmasdan, joriy holatni tekshirish xavfsizroq.

To‘liq ishlaydigan misol

Oldingi ikki darsdagi kod bitta dasturga birlashtirildi. Qisqalik uchun validatsiya soddalashtirilgan.

main.go
package main

import (
    "context"
    "encoding/json"
    "errors"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"
    "sync"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "golang.org/x/crypto/bcrypt"
)

// ---------- Foydalanuvchilar ----------

type User struct {
    ID           int64
    Email        string
    PasswordHash string
    Role         string
}

type UserRepo struct {
    mu      sync.RWMutex
    byID    map[int64]User
    byEmail map[string]int64
    nextID  int64
}

var errNotFound = errors.New("topilmadi")

func (r *UserRepo) Create(u User) (User, error) {
    r.mu.Lock()
    defer r.mu.Unlock()
    if _, ok := r.byEmail[u.Email]; ok {
        return User{}, errors.New("email band")
    }
    r.nextID++
    u.ID = r.nextID
    r.byID[u.ID] = u
    r.byEmail[u.Email] = u.ID
    return u, nil
}

func (r *UserRepo) ByEmail(email string) (User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    id, ok := r.byEmail[email]
    if !ok {
        return User{}, errNotFound
    }
    return r.byID[id], nil
}

func (r *UserRepo) ByID(ctx context.Context, id int64) (User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    u, ok := r.byID[id]
    if !ok {
        return User{}, errNotFound
    }
    return u, nil
}

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

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

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

func (s *TokenService) Issue(u User) (string, error) {
    now := time.Now()
    return jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
        Role: u.Role,
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   strconv.FormatInt(u.ID, 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
}

// ---------- Auth middleware ----------

type Principal struct {
    UserID int64
    Role   string
}

type principalKey struct{}

func PrincipalFrom(ctx context.Context) (Principal, bool) {
    p, ok := ctx.Value(principalKey{}).(Principal)
    return p, ok
}

type Auth struct {
    tokens *TokenService
}

func bearerToken(r *http.Request) (string, bool) {
    scheme, token, ok := strings.Cut(r.Header.Get("Authorization"), " ")
    if !ok || !strings.EqualFold(scheme, "Bearer") || token == "" {
        return "", false
    }
    return strings.TrimSpace(token), true
}

func unauthorized(w http.ResponseWriter, code, msg string) {
    w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
    writeError(w, http.StatusUnauthorized, code, msg)
}

func (a *Auth) Require(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        raw, ok := bearerToken(r)
        if !ok {
            unauthorized(w, "missing_token", "Authorization sarlavhasida Bearer token kerak")
            return
        }

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

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

        ctx := context.WithValue(r.Context(), principalKey{}, Principal{UserID: userID, Role: claims.Role})
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

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

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, code, msg string) {
    writeJSON(w, status, map[string]any{"error": map[string]string{"code": code, "message": msg}})
}

type credentials struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

type Handler struct {
    users     *UserRepo
    tokens    *TokenService
    dummyHash []byte
}

func (h *Handler) signup(w http.ResponseWriter, r *http.Request) {
    var c credentials
    if err := json.NewDecoder(r.Body).Decode(&c); err != nil || c.Email == "" || len(c.Password) < 8 {
        writeError(w, http.StatusBadRequest, "bad_request", "email va kamida 8 belgili parol kerak")
        return
    }
    hash, err := bcrypt.GenerateFromPassword([]byte(c.Password), bcrypt.DefaultCost)
    if err != nil {
        writeError(w, http.StatusBadRequest, "bad_request", "parol noto‘g‘ri")
        return
    }
    u, err := h.users.Create(User{
        Email:        strings.ToLower(strings.TrimSpace(c.Email)),
        PasswordHash: string(hash),
        Role:         "reader",
    })
    if err != nil {
        writeError(w, http.StatusConflict, "email_taken", "bu email bilan akkaunt mavjud")
        return
    }
    writeJSON(w, http.StatusCreated, map[string]any{"data": map[string]any{"id": u.ID, "email": u.Email}})
}

func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
    var c credentials
    if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
        writeError(w, http.StatusBadRequest, "bad_request", "JSON noto‘g‘ri")
        return
    }

    u, err := h.users.ByEmail(strings.ToLower(strings.TrimSpace(c.Email)))
    hash := h.dummyHash
    if err == nil {
        hash = []byte(u.PasswordHash)
    }
    if bcrypt.CompareHashAndPassword(hash, []byte(c.Password)) != nil || err != nil {
        unauthorized(w, "invalid_credentials", "email yoki parol noto‘g‘ri")
        return
    }

    token, err := h.tokens.Issue(u)
    if err != nil {
        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()),
    })
}

func (h *Handler) me(w http.ResponseWriter, r *http.Request) {
    p, ok := PrincipalFrom(r.Context())
    if !ok {
        writeError(w, http.StatusInternalServerError, "internal", "ichki server xatosi")
        return
    }
    u, err := h.users.ByID(r.Context(), p.UserID)
    if err != nil {
        unauthorized(w, "invalid_token", "foydalanuvchi topilmadi")
        return
    }
    writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": u.ID, "email": u.Email, "role": u.Role}})
}

func (h *Handler) listBooks(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, map[string]any{"data": []string{"O‘tkan kunlar", "Sarob"}})
}

func (h *Handler) createBook(w http.ResponseWriter, r *http.Request) {
    p, _ := PrincipalFrom(r.Context())
    writeJSON(w, http.StatusCreated, map[string]any{"data": map[string]any{"title": "Yangi kitob", "owner_id": p.UserID}})
}

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}
    dummy, _ := bcrypt.GenerateFromPassword([]byte("dummy-password"), bcrypt.DefaultCost)
    h := &Handler{
        users:     &UserRepo{byID: map[int64]User{}, byEmail: map[string]int64{}},
        tokens:    tokens,
        dummyHash: dummy,
    }
    auth := &Auth{tokens: tokens}

    mux := http.NewServeMux()
    mux.HandleFunc("POST /auth/signup", h.signup)
    mux.HandleFunc("POST /auth/login", h.login)
    mux.HandleFunc("GET /books", h.listBooks)
    mux.Handle("GET /me", auth.Require(http.HandlerFunc(h.me)))
    mux.Handle("POST /books", auth.Require(http.HandlerFunc(h.createBook)))

    log.Println("Server: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Sinab ko‘rish

export JWT_SECRET=$(openssl rand -hex 32)
go run main.go

Boshqa terminalda:

U=http://localhost:8080

curl -s -X POST $U/auth/signup -d '{"email":"[email protected]","password":"juda-maxfiy-parol"}'

TOKEN=$(curl -s -X POST $U/auth/login \
  -d '{"email":"[email protected]","password":"juda-maxfiy-parol"}' | jq -r .access_token)

echo $TOKEN
{"data":{"email":"[email protected]","id":1}}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicmVhZGVyIiwiaXNzIjoiYm9va3MtYXBpIiwic3ViIjoiMSIsImV4cCI6...

jq -r .access_token JSON javobdan faqat tokenni ajratib oladi va TOKEN o‘zgaruvchisiga yozadi.

Endi himoyalangan endpointlarga murojaat qilamiz:

curl -s $U/books
curl -s $U/me
curl -s $U/me -H "Authorization: Bearer $TOKEN"
curl -s -X POST $U/books -H "Authorization: Bearer $TOKEN"
curl -s $U/me -H "Authorization: Bearer ${TOKEN}x"
curl -s -i $U/me -H "Authorization: Basic YWxpOnBhcm9s" | head -3
{"data":["O‘tkan kunlar","Sarob"]}
{"error":{"code":"missing_token","message":"Authorization sarlavhasida Bearer token kerak"}}
{"data":{"email":"[email protected]","id":1,"role":"reader"}}
{"data":{"owner_id":1,"title":"Yangi kitob"}}
{"error":{"code":"invalid_token","message":"token yaroqsiz"}}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Www-Authenticate: Bearer error="invalid_token"
  • GET /books ochiq endpoint, token talab qilinmadi.
  • GET /me tokensiz 401 qaytardi.
  • Token bilan /me foydalanuvchi ma’lumotini qaytardi. POST /books esa kitob egasi sifatida joriy foydalanuvchi ID’sini oldi.
  • Tokenning oxiriga bitta belgi qo‘shilgani imzoni buzdi va token rad etildi.
  • Basic sxemasi qabul qilinmadi.

Ixtiyoriy autentifikatsiya

Ba’zi endpointlar hammaga ochiq, lekin tizimga kirgan foydalanuvchiga qo‘shimcha ma’lumot ko‘rsatadi. Masalan, kitoblar ro‘yxatida "siz bu kitobni saqlagansiz" belgisi. Buning uchun token bo‘lsa uni tekshiradigan, bo‘lmasa so‘rovni o‘tkazib yuboradigan middleware yoziladi:

func (a *Auth) Optional(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        raw, ok := bearerToken(r)
        if !ok {
            next.ServeHTTP(w, r) // token yo‘q — mehmon sifatida davom etadi
            return
        }
        claims, err := a.tokens.Parse(raw)
        if err != nil {
            unauthorized(w, "invalid_token", "token yaroqsiz")
            return
        }
        userID, _ := strconv.ParseInt(claims.Subject, 10, 64)
        ctx := context.WithValue(r.Context(), principalKey{}, Principal{UserID: userID, Role: claims.Role})
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

E’tibor bering: token yuborilgan, lekin yaroqsiz bo‘lsa, so‘rov mehmon sifatida davom ettirilmaydi, 401 qaytariladi. Aks holda muddati tugagan tokenli foydalanuvchi o‘zini tizimdan chiqib ketganini sezmay qolardi.

Xulosa

  • Auth middleware tokenni oladi, tekshiradi va foydalanuvchini contextga joylaydi. Muvaffaqiyatsizlikda 401 qaytaradi.
  • Context kaliti eksport qilinmagan turdan yaratiladi. Contextdan o‘qish yordamchi funksiya orqali bo‘ladi.
  • Muddati tugagan token uchun alohida xato kodi qaytariladi, boshqa xatolarning tafsiloti yashiriladi.
  • Metodli patternlar yordamida bitta yo‘lning turli metodlariga turlicha himoya qo‘yiladi.
  • Muhim amallarda foydalanuvchi holati bazadan qayta tekshiriladi.

Shu bilan autentifikatsiya bo‘limi tugadi. Endi server foydalanuvchini taniydi. Keyingi bo‘limda "Unga nimaga ruxsat bor?" degan savolga javob beramiz — avtorizatsiyani o‘rganamiz.