احراز هویت JWT در گولنگ - آموزش امنیت در Go
2025/11/24JWT (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"}'نکات امنیتی
- کلید مخفی قوی: از کلید تصادفی حداقل 32 کاراکتر استفاده کنید
- HTTPS: همیشه از HTTPS استفاده کنید
- مدت اعتبار کوتاه: Access Token کوتاه (15 دقیقه)
- Blacklist: برای logout، توکنها را blacklist کنید
- Rate Limiting: محدودیت تعداد درخواست login