GoCasts آموزش Go به زبان ساده

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی

احراز هویت JWT در گولنگ - آموزش امنیت در Go

JWT (JSON Web Token) یکی از محبوب‌ترین روش‌های احراز هویت در API‌های مدرن است. در این مقاله، پیاده‌سازی کامل JWT در Go را یاد می‌گیریم.

JWT چیست؟

JWT یک استاندارد برای انتقال امن اطلاعات بین دو طرف است. هر JWT از سه بخش تشکیل شده:

header.payload.signature
xxxxx.yyyyy.zzzzz
  • Header: نوع توکن و الگوریتم امضا
  • Payload: داده‌ها (claims)
  • Signature: امضای دیجیتال

راه‌اندازی پروژه

mkdir jwt-auth-demo && cd jwt-auth-demo
go mod init github.com/username/jwt-auth-demo

# نصب پکیج‌ها
go get -u github.com/gin-gonic/gin
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt

ساختار پروژه

jwt-auth-demo/
├── main.go
├── auth/
│   └── jwt.go
├── handlers/
│   └── auth_handler.go
├── middleware/
│   └── auth_middleware.go
├── models/
│   └── user.go
└── go.mod

تعریف Model

models/user.go:

package models

import "time"

type User struct {
    ID        int       `json:"id"`
    Email     string    `json:"email"`
    Password  string    `json:"-"` // نمایش داده نشود
    Name      string    `json:"name"`
    Role      string    `json:"role"`
    CreatedAt time.Time `json:"created_at"`
}

type LoginRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

type RegisterRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
    Name     string `json:"name" binding:"required"`
}

type TokenResponse struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    ExpiresIn    int64  `json:"expires_in"`
}

// شبیه‌سازی دیتابیس
var Users = []User{}
var NextUserID = 1

سرویس JWT

auth/jwt.go:

package auth

import (
    "errors"
    "fmt"
    "strconv"
    "time"

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

// کلید مخفی - در پروژه واقعی از متغیر محیطی بخوانید
var (
    AccessTokenSecret  = []byte("your-access-token-secret-key")
    RefreshTokenSecret = []byte("your-refresh-token-secret-key")
)

// مدت اعتبار توکن‌ها
const (
    AccessTokenExpiry  = 15 * time.Minute
    RefreshTokenExpiry = 7 * 24 * time.Hour
)

// Claims ساختار داده‌های توکن
type Claims struct {
    UserID int    `json:"user_id"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

// تولید Access Token
func GenerateAccessToken(userID int, email, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  email,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenExpiry)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "gocasts-api",
        },
    }

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

// تولید Refresh Token
func GenerateRefreshToken(userID int) (string, error) {
    claims := jwt.RegisteredClaims{
        Subject:   fmt.Sprintf("%d", userID),
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenExpiry)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
        Issuer:    "gocasts-api",
    }

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

// اعتبارسنجی Access Token
func ValidateAccessToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(
        tokenString,
        &Claims{},
        func(token *jwt.Token) (interface{}, error) {
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, errors.New("روش امضا نامعتبر است")
            }
            return AccessTokenSecret, nil
        },
    )

    if err != nil {
        return nil, err
    }

    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, errors.New("توکن نامعتبر است")
    }

    return claims, nil
}

// اعتبارسنجی Refresh Token
func ValidateRefreshToken(tokenString string) (int, error) {
    token, err := jwt.ParseWithClaims(
        tokenString,
        &jwt.RegisteredClaims{},
        func(token *jwt.Token) (interface{}, error) {
            return RefreshTokenSecret, nil
        },
    )

    if err != nil {
        return 0, err
    }

    claims, ok := token.Claims.(*jwt.RegisteredClaims)
    if !ok || !token.Valid {
        return 0, errors.New("توکن نامعتبر است")
    }

    userID, _ := strconv.Atoi(claims.Subject)
    return userID, nil
}

Handler‌های احراز هویت

handlers/auth_handler.go:

package handlers

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/username/jwt-auth-demo/auth"
    "github.com/username/jwt-auth-demo/models"
    "golang.org/x/crypto/bcrypt"
)

// ثبت‌نام
func Register(c *gin.Context) {
    var req models.RegisterRequest

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // بررسی تکراری نبودن ایمیل
    for _, user := range models.Users {
        if user.Email == req.Email {
            c.JSON(http.StatusConflict, gin.H{
                "error": "این ایمیل قبلاً ثبت شده است",
            })
            return
        }
    }

    // هش کردن رمز عبور
    hashedPassword, err := bcrypt.GenerateFromPassword(
        []byte(req.Password),
        bcrypt.DefaultCost,
    )
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "خطا در پردازش رمز عبور",
        })
        return
    }

    // ایجاد کاربر جدید
    user := models.User{
        ID:        models.NextUserID,
        Email:     req.Email,
        Password:  string(hashedPassword),
        Name:      req.Name,
        Role:      "user",
        CreatedAt: time.Now(),
    }
    models.NextUserID++
    models.Users = append(models.Users, user)

    c.JSON(http.StatusCreated, gin.H{
        "message": "ثبت‌نام با موفقیت انجام شد",
        "user": gin.H{
            "id":    user.ID,
            "email": user.Email,
            "name":  user.Name,
        },
    })
}

// ورود
func Login(c *gin.Context) {
    var req models.LoginRequest

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // پیدا کردن کاربر
    var user *models.User
    for i := range models.Users {
        if models.Users[i].Email == req.Email {
            user = &models.Users[i]
            break
        }
    }

    if user == nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "ایمیل یا رمز عبور اشتباه است",
        })
        return
    }

    // بررسی رمز عبور
    if err := bcrypt.CompareHashAndPassword(
        []byte(user.Password),
        []byte(req.Password),
    ); err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "ایمیل یا رمز عبور اشتباه است",
        })
        return
    }

    // تولید توکن‌ها
    accessToken, err := auth.GenerateAccessToken(user.ID, user.Email, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "خطا در تولید توکن",
        })
        return
    }

    refreshToken, err := auth.GenerateRefreshToken(user.ID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "خطا در تولید توکن",
        })
        return
    }

    c.JSON(http.StatusOK, models.TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        ExpiresIn:    int64(auth.AccessTokenExpiry.Seconds()),
    })
}

// تازه‌سازی توکن
func RefreshToken(c *gin.Context) {
    var req struct {
        RefreshToken string `json:"refresh_token" binding:"required"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // اعتبارسنجی refresh token
    userID, err := auth.ValidateRefreshToken(req.RefreshToken)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "توکن نامعتبر یا منقضی شده",
        })
        return
    }

    // پیدا کردن کاربر
    var user *models.User
    for i := range models.Users {
        if models.Users[i].ID == userID {
            user = &models.Users[i]
            break
        }
    }

    if user == nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "کاربر یافت نشد",
        })
        return
    }

    // تولید توکن جدید
    accessToken, _ := auth.GenerateAccessToken(user.ID, user.Email, user.Role)
    refreshToken, _ := auth.GenerateRefreshToken(user.ID)

    c.JSON(http.StatusOK, models.TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        ExpiresIn:    int64(auth.AccessTokenExpiry.Seconds()),
    })
}

// دریافت اطلاعات کاربر فعلی
func GetMe(c *gin.Context) {
    userID := c.GetInt("userID")

    for _, user := range models.Users {
        if user.ID == userID {
            c.JSON(http.StatusOK, gin.H{
                "user": gin.H{
                    "id":    user.ID,
                    "email": user.Email,
                    "name":  user.Name,
                    "role":  user.Role,
                },
            })
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{
        "error": "کاربر یافت نشد",
    })
}

Middleware احراز هویت

middleware/auth_middleware.go:

package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/username/jwt-auth-demo/auth"
)

func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        // دریافت توکن از header
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "توکن الزامی است",
            })
            c.Abort()
            return
        }

        // بررسی فرمت Bearer
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "فرمت توکن نامعتبر است",
            })
            c.Abort()
            return
        }

        // اعتبارسنجی توکن
        claims, err := auth.ValidateAccessToken(parts[1])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "توکن نامعتبر یا منقضی شده",
            })
            c.Abort()
            return
        }

        // ذخیره اطلاعات کاربر در context
        c.Set("userID", claims.UserID)
        c.Set("email", claims.Email)
        c.Set("role", claims.Role)

        c.Next()
    }
}

// Middleware بررسی نقش
func RequireRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole := c.GetString("role")

        for _, role := range roles {
            if userRole == role {
                c.Next()
                return
            }
        }

        c.JSON(http.StatusForbidden, gin.H{
            "error": "دسترسی غیرمجاز",
        })
        c.Abort()
    }
}

فایل اصلی

main.go:

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/username/jwt-auth-demo/handlers"
    "github.com/username/jwt-auth-demo/middleware"
)

func main() {
    r := gin.Default()

    // مسیرهای عمومی
    auth := r.Group("/auth")
    {
        auth.POST("/register", handlers.Register)
        auth.POST("/login", handlers.Login)
        auth.POST("/refresh", handlers.RefreshToken)
    }

    // مسیرهای محافظت شده
    api := r.Group("/api")
    api.Use(middleware.JWTAuth())
    {
        api.GET("/me", handlers.GetMe)

        // فقط ادمین
        admin := api.Group("/admin")
        admin.Use(middleware.RequireRole("admin"))
        {
            admin.GET("/users", func(c *gin.Context) {
                c.JSON(200, gin.H{"users": models.Users})
            })
        }
    }

    r.Run(":8080")
}

تست با cURL

# ثبت‌نام
curl -X POST http://localhost:8080/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"ali@example.com","password":"123456","name":"علی"}'

# ورود
curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"ali@example.com","password":"123456"}'

# دریافت اطلاعات کاربر (با توکن)
curl http://localhost:8080/api/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# تازه‌سازی توکن
curl -X POST http://localhost:8080/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'

نکات امنیتی

  1. کلید مخفی قوی: از کلید تصادفی حداقل 32 کاراکتر استفاده کنید
  2. HTTPS: همیشه از HTTPS استفاده کنید
  3. مدت اعتبار کوتاه: Access Token کوتاه (15 دقیقه)
  4. Blacklist: برای logout، توکن‌ها را blacklist کنید
  5. Rate Limiting: محدودیت تعداد درخواست login

قدم‌های بعدی

منابع

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی