احراز هویت jwt در go با gin چگونه است؟

احراز هویت JWT در Go با Gin چگونه است؟

مقدمه

توکن وب JSON (JWT) یک روش فشرده و امضاشده برای انتقال امن اطلاعات بین طرفین به‌صورت یک شیء JSON است. در APIهای وب، JWTها گزینه‌ای رایج برای احراز هویت هستند، زیرا سرور می‌تواند بدون اینکه چیزی را در پایگاه داده جست‌وجو کند، آن‌ها را اعتبارسنجی کند.

این روش بین توسعه‌دهندگان محبوب است چون:

  • بدون وضعیت (Stateless): یک JWT معتبر تمام داده‌های لازم برای اعتبارسنجی را در خود دارد، بنابراین نیازی به لایه ذخیره‌سازی پایدار نیست.

  • مقاوم در برابر دستکاری: امضای JWT همیشه قبل از اعتماد به payload بررسی می‌شود.

  • انقضای قابل تنظیم: می‌توان طول عمر کوتاهی تعیین کرد تا اگر توکن لو رفت، میزان خسارت محدود شود.

در این آموزش، یک سیستم احراز هویت امن در Go با استفاده از فریم‌ورک Gin، توکن‌های JWT و Redis ساخته می‌شود تا ذخیره‌سازی توکن، چرخش (rotation) و ابطال (revocation) مدیریت شود.

معرفی JWT

JWT از چه چیزهایی تشکیل می‌شود

یک JWT از سه بخش Base64 URL-encoded تشکیل می‌شود که با نقطه به هم متصل شده‌اند:

<header>.<payload>.<signature>
  • Header: نوع توکن و الگوریتم امضای استفاده‌شده. نوع توکن معمولاً “JWT” است و الگوریتم امضا می‌تواند HMAC یا SHA256 باشد.

  • Payload: بخش دوم توکن که شامل claimها است. این claimها شامل داده‌های مخصوص اپلیکیشن (مثل user id، username)، زمان انقضای توکن (exp)، صادرکننده (iss)، موضوع (sub) و موارد دیگر می‌شود.

  • Signature: هدرِ کدگذاری‌شده، payload کدگذاری‌شده و یک secret که ارائه می‌شود برای ساخت امضا استفاده می‌شوند.

برای درک مفاهیم بالا از یک توکن ساده استفاده می‌شود. این توکن با کلید مخفی “my-secret-key” امضا شده است.

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.bK73DLHVXVmtLXBYwl_TVLVf21OEJ0bzfZHXRbiAboA

Secret: my-secret-key

نگران نباشید، این توکن نامعتبر است و روی هیچ اپلیکیشن تولیدی کار نمی‌کند.

به JWT Debugger بروید و توکن را در بخش “Encoded” قرار دهید، secret را در بخش “Verify Signature” وارد کنید و بررسی کنید که عبارت “Signature Verified” نمایش داده شود.

احراز هویت jwt در go با gin چگونه است؟

انواع توکن‌ها در این راهنما

از آن‌جا که JWT می‌تواند طوری تنظیم شود که بعد از یک مدت مشخص منقضی شود، در این اپلیکیشن دو نوع توکن در نظر گرفته می‌شود:

  • Access token: کوتاه‌مدت (حدود ~۱۵ دقیقه). در هر درخواست ارسال می‌شود.

  • Refresh token: بلندمدت‌تر (حدود ~۷ روز). از طریق یک endpoint اختصاصی با یک جفت جدید access+refresh تعویض می‌شود. چرخش (rotation) پیاده‌سازی می‌شود (refresh قدیمی بعد از استفاده نامعتبر می‌شود).

JWT را کجا ذخیره کنیم

برای یک اپلیکیشن وب تولیدی، به‌شدت توصیه می‌شود JWTها در یک کوکی HttpOnly ذخیره شوند:

احراز هویت jwt در go با gin چگونه است؟

کوکی‌ها با HttpOnly; Secure; SameSite=Lax (یا SameSite=None; Secure برای حالت cross-site) تنظیم می‌شوند و در API نیز CORS همراه با credentials فعال می‌شود.

انتخاب‌های مختلف الگوریتم

الگوریتم‌های امضای JWT مشخص می‌کنند توکن‌ها از نظر رمزنگاری چگونه امن می‌شوند. دو الگوریتمی که استفاده می‌شود:

  • HS256 (HMAC): یک secret مشترک؛ برای یک سرویس تکی ساده است (در این آموزش از HS256 استفاده می‌شود).

  • RS256 / EdDSA: کلیدهای نامتقارن؛ زمانی بهتر است که چند سرویس باید بدون اشتراک‌گذاری secret بتوانند اعتبارسنجی انجام دهند (همچنین یک مثال RS256 برای Vonage APIs نشان داده می‌شود).

نمای کلی پروژه

قبل از ورود به کد، باید مشخص شود چه چیزی ساخته می‌شود. یک API کوچک و امن برای یک اپلیکیشن فهرست کارها (to-do list) ساخته می‌شود. کاربران باید وارد شوند، کارهای شخصی خود را ببینند و خارج شوند. پیچیده نیست، اما باید آن‌قدر امن باشد که بتواند با اطلاعات واقعی کار کند.

در این آموزش، با Go و فریم‌ورک Gin یک سیستم احراز هویت حداقلی مبتنی بر JWT ساخته می‌شود. برای ساده‌سازی، به پایگاه داده واقعی وصل نمی‌شود. به‌جای آن، یک کاربر «نمونه» در کد تعریف می‌شود. مسیر /login یک نام کاربری و رمز عبور می‌گیرد، آن را با کاربر داخل حافظه بررسی می‌کند و دو توکن صادر می‌کند: یک access token کوتاه‌مدت و یک refresh token بلندمدت‌تر.

بعد از ورود، مسیر /todo فقط در صورتی پاسخ می‌دهد که یک access token معتبر ارسال شود. همچنین یک مسیر /logout برای ابطال فوری توکن‌ها (با Redis) وجود دارد و مسیر /token/refresh برای چرخش refresh tokenها و زنده نگه داشتن سشن‌ها به‌شکل امن پیاده‌سازی می‌شود.

چرا Redis؟

JWTها معمولاً بدون وضعیت هستند. یعنی بعد از صدور، سرور نمی‌تواند آن‌ها را «پس بگیرد». با ذخیره کردن jti (شناسه یکتا) در Redis همراه با Time to Live (TTL)، امکان‌های زیر فراهم می‌شود:

  • ابطال فوری در زمان logout

  • چرخش refresh tokenها برای جلوگیری از استفاده مجدد

  • نگه داشتن سشن‌های چند دستگاه بدون باطل کردن همه آن‌ها

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

شروع با ساخت و مقداردهی اولیه پروژه:

mkdir jwt-todo && cd jwt-todo
go mod init jwt-todo

نصب وابستگی‌ها:

  • gin: فریم‌ورک HTTP سریع و سبک برای ساخت routeهای API

  • golang-jwt: ساخت، امضا و اعتبارسنجی JWT برای جریان احراز هویت

  • go-redis: کلاینت Redis برای Go، برای ذخیره و ابطال فوری توکن‌ها

  • uuid: تولید شناسه‌های یکتای توکن (jti) برای هر JWT جهت ابطال دقیق

  • gin-contrib/cors: میان‌افزار برای تنظیم قواعد CORS جهت ارتباط امن کلاینت مرورگر با API

  • godotenv: بارگذاری متغیرهای محیطی از فایل .env

کدها و دستورات زیر دقیقاً همان‌طور که هستند باقی می‌مانند:

go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v5
go get github.com/redis/go-redis/v9
go get github.com/google/uuid
go get github.com/gin-contrib/cors
go get github.com/joho/godotenv

ساختار پروژه:

├── cmd/
│ └── server/main.go
├── internal/
│ ├── auth/auth.go
│ ├── auth/middleware.go
│ ├── store/redis.go
│ └── todo/handler.go
├── go.mod
└── .env

ایجاد فایل‌ها:

mkdir -p cmd/server internal/auth internal/store internal/todo && \
touch cmd/server/main.go \
internal/auth/auth.go \
internal/auth/middleware.go \
internal/store/redis.go \
internal/todo/handler.go \
.env \
.gitignore

افزودن متغیرهای محیطی

اپلیکیشن برای اجرای امن به چند مقدار محرمانه و تنظیمات نیاز دارد. این موارد در توسعه داخل فایل .env قرار می‌گیرند و هنگام استقرار به‌صورت متغیر محیطی واقعی تعریف می‌شوند.

تولید secretهای قوی

دو کلید امضا لازم است: یکی برای access token و یکی برای refresh token. این کلیدها باید طولانی، تصادفی و غیرقابل حدس باشند. یک قانون خوب: حداقل ۳۲ بایت تصادفی بودن.

روی macOS / Linux:

# ۳۲ bytes, Base64-encoded
openssl rand -base64 32

روی Windows (PowerShell):

[Convert]::ToBase64String((۱..۳۲ | % {Get-Random -Max ۲۵۶}))

هر دستور یک رشته امن خروجی می‌دهد، مثلاً:

lbme36L7N2gqbwR5lOKg1BkIcVu+GTk8K1/b+lQUlng=

دستور مربوطه را دو بار اجرا کنید. یک مقدار برای ACCESS_SECRET و مقدار دیگر برای REFRESH_SECRET استفاده می‌شود.

افزودن متغیرهای ENV

در فایل .env موارد زیر اضافه می‌شود:

ACCESS_SECRET=dev-access-secret-change-me
REFRESH_SECRET=dev-refresh-secret-change-me
REDIS_ADDR=localhost:6379
FRONTEND_ORIGIN=http://localhost:5173

توضیح هر مورد:

  • ACCESS_SECRET: برای امضای access tokenهای کوتاه‌مدت (~۱۵ دقیقه)

  • REFRESH_SECRET: برای امضای refresh tokenهای بلندمدت (~۷ روز)

  • REDIS_ADDR: محل Redis را به اپلیکیشن اعلام می‌کند

  • FRONTEND_ORIGIN: آدرس فرانت‌اند مجاز برای CORS

Note: همیشه .env را به .gitignore اضافه کنید تا secrets به‌طور تصادفی وارد version control نشود.

اجرای Redis به‌صورت محلی

برای استفاده از Redis در بخش بعد، لازم است Redis در پس‌زمینه اجرا شود.

macOS (Homebrew)

اگر Homebrew نصب است:

brew install redis
brew services start redis

Windows

ساده‌ترین راه استفاده از Docker (پیشنهادی) یا WSL است. بعد از نصب و اجرای Docker Desktop:

docker run --name jwt-redis -p 6379:6379 -d redis:7

این کار آخرین ایمیج Redis 7 را دریافت می‌کند، در پس‌زمینه اجرا می‌کند و روی پورت ۶۳۷۹ در دسترس قرار می‌دهد.

تنظیم Redis

این بخش یک wrapper کوچک پیرامون کلاینت Redis می‌سازد تا اپلیکیشن بتواند ساده و تمیز با Redis تعامل کند. با تعریف یک struct به نام Redis که یک *redis.Client را نگه می‌دارد، روشی قابل استفاده مجدد برای مدیریت اتصال Redis در سراسر اپ ایجاد می‌شود. این کد پکیج رسمی go-redis v9 را ایمپورت می‌کند.

در اینجا، فیلد Client یک اتصال زنده Redis را نگه می‌دارد که در بخش دیگری از کد پیکربندی شده است. با قرار دادن این قسمت در پکیج جداگانه (store)، منطق ذخیره‌سازی از بقیه اپ جدا می‌شود و نگهداری و تست ساده‌تر خواهد شد.

کد زیر بدون هیچ تغییری همان‌طور که هست باقی می‌ماند:

// internal/store/redis.go:
package store
import (
“context”
“os”
“time”

“github.com/redis/go-redis/v9”
)

type Redis struct{ Client *redis.Client }

func NewRedis() *Redis {
addr := os.Getenv(“REDIS_ADDR”)
if addr == “” {
addr = “localhost:6379”
}
rdb := redis.NewClient(&redis.Options{Addr: addr})
return &Redis{Client: rdb}
}

func (r *Redis) SetJTI(ctx context.Context, key, userID string, exp time.Time) error {
return r.Client.Set(ctx, key, userID, time.Until(exp)).Err()
}

func (r *Redis) DelJTI(ctx context.Context, key string) error {
return r.Client.Del(ctx, key).Err()
}

func (r *Redis) GetUserByJTI(ctx context.Context, key string) (string, error) {
return r.Client.Get(ctx, key).Result()
}

تولید احراز هویت JWT با Go

صدور و ذخیره‌سازی توکن‌ها

در این مرحله access tokenهای کوتاه‌مدت و refresh tokenهای بلندمدت با استفاده از claimهای ثبت‌شده استاندارد JWT مانند issuer، audience، subject و expiration ساخته می‌شوند. پکیج golang-jwt فرآیند امضا را مدیریت می‌کند و با استفاده از secretهای امن ذخیره‌شده، مانع دستکاری توکن‌ها می‌شود. پس از تولید، هر دو توکن در Redis ذخیره می‌شوند تا بعداً امکان اعتبارسنجی یا ابطال آن‌ها فراهم شود. این کار یک روش امن و متمرکز برای مدیریت سشن‌های فعال ایجاد می‌کند.

// internal/auth/auth.go
package auth
import (
“context”
“errors”
“net/http”
“os”
“time”

“jwt-todo/internal/store”

“github.com/gin-gonic/gin”
“github.com/golang-jwt/jwt/v5”
“github.com/google/uuid”
)

type Tokens struct {
Access string
Refresh string
JTIAcc string
JTIRef string
ExpAcc time.Time
ExpRef time.Time
UserID string
Issuer string
Audience string
}

func IssueTokens(userID string) (*Tokens, error) {
now := time.Now().UTC()
t := &Tokens{
UserID: userID,
JTIAcc: uuid.NewString(),
JTIRef: uuid.NewString(),
ExpAcc: now.Add(۱۵ * time.Minute),
ExpRef: now.Add(۷ * ۲۴ * time.Hour),
Issuer: “jwt-todo-app”,
Audience: “jwt-todo-client”,
}

acc := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Subject: userID,
ID: t.JTIAcc,
Issuer: t.Issuer,
Audience: jwt.ClaimStrings{t.Audience},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(t.ExpAcc),
})

ref := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Subject: userID,
ID: t.JTIRef,
Issuer: t.Issuer,
Audience: jwt.ClaimStrings{t.Audience},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(t.ExpRef),
})

var err error
t.Access, err = acc.SignedString([]byte(os.Getenv(“ACCESS_SECRET”)))
if err != nil {
return nil, err
}
t.Refresh, err = ref.SignedString([]byte(os.Getenv(“REFRESH_SECRET”)))
if err != nil {
return nil, err
}
return t, nil
}

func Persist(ctx context.Context, r *store.Redis, t *Tokens) error {
if err := r.SetJTI(ctx, “access:”+t.JTIAcc, t.UserID, t.ExpAcc); err != nil {
return err
}
if err := r.SetJTI(ctx, “refresh:”+t.JTIRef, t.UserID, t.ExpRef); err != nil {
return err
}
return nil
}

func SetAuthCookies(c *gin.Context, t *Tokens) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(“access_token”, t.Access, int(time.Until(t.ExpAcc).Seconds()), “/”, “”, true, true)
c.SetCookie(“refresh_token”, t.Refresh, int(time.Until(t.ExpRef).Seconds()), “/”, “”, true, true)
}

func ClearAuthCookies(c *gin.Context) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(“access_token”, “”, , “/”, “”, true, true)
c.SetCookie(“refresh_token”, “”, , “/”, “”, true, true)
}

func ParseAccess(tokenStr string) (*jwt.RegisteredClaims, error) {
secret := os.Getenv(“ACCESS_SECRET”)
return parseWithSecret(tokenStr, secret)
}

func ParseRefresh(tokenStr string) (*jwt.RegisteredClaims, error) {
secret := os.Getenv(“REFRESH_SECRET”)
return parseWithSecret(tokenStr, secret)
}

func parseWithSecret(tokenStr, secret string) (*jwt.RegisteredClaims, error) {
if secret == “” {
return nil, errors.New(“jwt secret not configured”)
}

parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))

token, err := parser.ParseWithClaims(tokenStr, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
// Extra safety: ensure HMAC family
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New(“unexpected signing method”)
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}

claims, ok := token.Claims.(*jwt.RegisteredClaims)
if !ok || !token.Valid {
return nil, errors.New(“invalid token”)
}

if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
return nil, errors.New(“token expired”)
}

return claims, nil
}

محافظت از مسیرها با Middleware

این مرحله تضمین می‌کند تنها درخواست‌های کاربران احراز هویت‌شده به برخی endpointها دسترسی دارند. middleware هر درخواست را رهگیری می‌کند، وجود یک access token معتبر را بررسی می‌کند و همچنین چک می‌کند که توکن در Redis باطل نشده باشد.

اگر توکن همه بررسی‌ها را پاس کند، درخواست به handler مقصد می‌رود؛ در غیر این صورت با خطای مناسب مسدود می‌شود. این کار منطق احراز هویت را متمرکز می‌کند تا لازم نباشد در هر route بررسی توکن تکرار شود و در نتیجه کد تمیزتر و امن‌تر می‌شود.

کد زیر بدون هیچ تغییری همان‌طور که هست باقی می‌ماند:

// internal/auth/middleware.go
package auth
import (
“context”
“errors”
“net/http”
“strings”

“jwt-todo/internal/store”

“github.com/gin-gonic/gin”
)

func bearerFromHeader(c *gin.Context) string {
h := c.GetHeader(“Authorization”)
if strings.HasPrefix(h, “Bearer “) {
return strings.TrimPrefix(h, “Bearer “)
}
return “”
}

func AuthMiddleware(r *store.Redis) gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr, _ := c.Cookie(“access_token”)
if tokenStr == “” {
tokenStr = bearerFromHeader(c)
}
if tokenStr == “” {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{“error”: “missing token”})
return
}

claims, err := ParseAccess(tokenStr)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{“error”: “invalid token”})
return
}

ctx := context.Background()
if _, err := r.GetUserByJTI(ctx, “access:”+claims.ID); err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{“error”: “token revoked”})
return
}

c.Set(“userID”, claims.Subject)
c.Next()
}
}

func MustCookie(c *gin.Context, name string) (string, error) {
val, err := c.Cookie(name)
if err != nil || val == “” {
return “”, errors.New(“missing cookie: “ + name)
}
return val, nil
}

یک مسیر محافظت‌شده نمونه: /todo

این مسیر نشان می‌دهد یک endpoint احراز هویت‌شده چگونه کار می‌کند. handler مربوط به Create مقدار userID را از توکن اعتبارسنجی‌شده (که توسط middleware تنظیم شده) می‌خواند و آن را با payload مربوط به todo ترکیب می‌کند. به‌جای ذخیره در پایگاه داده، همین داده به‌صورت JSON برگردانده می‌شود تا بتوان بررسی کرد احراز هویت درست کار می‌کند. در یک اپلیکیشن واقعی، اینجا همان جایی است که منطق ذخیره todo در دیتابیس اضافه می‌شود.

کد زیر بدون هیچ تغییری همان‌طور که هست باقی می‌ماند:

// internal/todo/handler.go
package todo
import (
“net/http”

“github.com/gin-gonic/gin”
)

type Todo struct {
UserID string `json:”user_id”`
Title string `json:”title”`
}

func Create() gin.HandlerFunc {
return func(c *gin.Context) {
var in struct {
Title string `json:”title” binding:”required”`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{“error”: “invalid json”})
return
}
userID, _ := c.Get(“userID”)
td := Todo{UserID: userID.(string), Title: in.Title}
// Here you’d save to a DB; we’ll just echo it back
c.JSON(http.StatusCreated, td)
}
}

اتصال همه بخش‌ها به هم

در مرحله نهایی، همه اجزا (ذخیره‌ساز Redis، منطق احراز هویت، middleware و handlerهای route) در یک اپلیکیشن Gin قابل اجرا کنار هم قرار می‌گیرند. فایل main.go وابستگی‌ها را مقداردهی می‌کند، routeها را تنظیم می‌کند و سرور HTTP را بالا می‌آورد. این همان چسبی است که تمام بخش‌های اپ را به هم متصل می‌کند تا احراز هویت، ذخیره توکن و routeهای محافظت‌شده از ابتدا تا انتها درست کار کنند.

// cmd/server/main.go

package main

import (
“context”
“log”
“net/http”
“os”

“github.com/gin-contrib/cors”
“github.com/gin-gonic/gin”
“github.com/joho/godotenv”

“jwt-todo/internal/auth”
“jwt-todo/internal/store”
“jwt-todo/internal/todo”
)

type User struct {
ID string `json:”id”`
Username string `json:”username”`
Password string `json:”password”`
}

var demoUser = User{ID: “۱”, Username: “user”, Password: “pass”}

func main() {
_ = godotenv.Load()

for _, k := range []string{“ACCESS_SECRET”, “REFRESH_SECRET”} {
if os.Getenv(k) == “” {
log.Fatalf(“%s not set”, k)
}
}

rds := store.NewRedis()
r := gin.Default()

r.Use(cors.New(cors.Config{
AllowOrigins: []string{os.Getenv(“FRONTEND_ORIGIN”)},
AllowMethods: []string{“GET”, “POST”},
AllowHeaders: []string{“Content-Type”, “Authorization”},
AllowCredentials: true,
}))

r.POST(“/login”, func(c *gin.Context) {
var in struct {
Username string `json:”username”`
Password string `json:”password”`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{“error”: “invalid json”})
return
}
if in.Username != demoUser.Username || in.Password != demoUser.Password {
c.JSON(http.StatusUnauthorized, gin.H{“error”: “invalid credentials”})
return
}

toks, err := auth.IssueTokens(demoUser.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{“error”: “could not issue tokens”})
return
}
if err := auth.Persist(c, rds, toks); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{“error”: “could not persist tokens”})
return
}
auth.SetAuthCookies(c, toks)
c.JSON(http.StatusOK, gin.H{“ok”: true})
})

r.POST(“/todo”, auth.AuthMiddleware(rds), todo.Create())

r.POST(“/logout”, auth.AuthMiddleware(rds), func(c *gin.Context) {
acc, _ := c.Cookie(“access_token”)
ref, _ := c.Cookie(“refresh_token”)
ctx := context.Background()

if acc != “” {
if claims, err := auth.ParseAccess(acc); err == nil {
_ = rds.DelJTI(ctx, “access:”+claims.ID)
}
}
if ref != “” {
if claims, err := auth.ParseRefresh(ref); err == nil {
_ = rds.DelJTI(ctx, “refresh:”+claims.ID)
}
}
auth.ClearAuthCookies(c)
c.JSON(http.StatusOK, gin.H{“ok”: true})
})

r.POST(“/token/refresh”, func(c *gin.Context) {
ref, err := auth.MustCookie(c, “refresh_token”)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{“error”: “missing refresh token”})
return
}
claims, err := auth.ParseRefresh(ref)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{“error”: “invalid refresh token”})
return
}
ctx := context.Background()
if _, err := rds.GetUserByJTI(ctx, “refresh:”+claims.ID); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{“error”: “refresh revoked”})
return
}
_ = rds.DelJTI(ctx, “refresh:”+claims.ID)

toks, err := auth.IssueTokens(claims.Subject)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{“error”: “could not issue new tokens”})
return
}
if err := auth.Persist(ctx, rds, toks); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{“error”: “could not persist new tokens”})
return
}
auth.SetAuthCookies(c, toks)
c.JSON(http.StatusCreated, gin.H{“ok”: true})
})

log.Fatal(r.Run(“:۸۰۸۰”))
}


تست احراز هویت JWT با cURL

حالا که اپلیکیشن و Redis در حال اجرا هستند، چرخه کامل احراز هویت بررسی می‌شود: ورود، ساخت todo، خروج، شکست خوردن یک درخواست احراز هویت‌شده، و refresh کردن توکن‌ها.

ابتدا سرور Gin اجرا می‌شود:

go run ./cmd/server

اگر همه‌چیز درست باشد، Gin routeها را چاپ می‌کند و روی :۸۰۸۰ گوش می‌دهد.

تست اپلیکیشن Gin

اطمینان حاصل شود که هر مرحله در یک تب جدا از تب سرور اجرا می‌شود.

تست ۱: ورود و ذخیره کوکی‌ها

این مرحله ورود کاربر را شبیه‌سازی می‌کند و access_token و refresh_token را برای درخواست‌های بعدی در cookies.txt ذخیره می‌کند.

curl -i -c cookies.txt \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"pass"}' \
http://localhost:8080/login

خروجی مورد انتظار:

  • HTTP 200 OK

  • دو هدر Set-Cookie (access_token, refresh_token)

  • JSON: {“ok”:true}

تست ۲: ساخت یک Todo (احراز هویت‌شده)

curl -i -b cookies.txt \
-H "Content-Type: application/json" \
-d '{"title":"write secure JWT blog"}' \
http://localhost:8080/todo

خروجی مورد انتظار:

  • HTTP 201 Created

  • JSON شامل user_id و title

تست ۳: خروج (ابطال توکن‌ها)

curl -i -b cookies.txt -X POST http://localhost:8080/logout

خروجی مورد انتظار:

  • HTTP 200 OK

  • کوکی‌های access_token و refresh_token خالی می‌شوند و Max-Age=0 می‌گیرند.

تست ۴: تلاش مجدد (باید شکست بخورد)

حالا که توکن‌ها باطل شده‌اند، یک درخواست ساخت باید شکست بخورد:

curl -i -b cookies.txt \
-H "Content-Type: application/json" \
-d '{"title":"should fail"}' \
http://localhost:8080/todo

خروجی مورد انتظار:

  • HTTP 401 Unauthorized

  • JSON: {“error”:”token revoked”}

تست ۵: Refresh توکن‌ها (گرفتن یک جفت جدید)

این تست بعد از ورود دوباره انجام می‌شود.

curl -i -b cookies.txt -c cookies.txt \
-X POST http://localhost:8080/token/refresh

خروجی مورد انتظار:

  • HTTP 201 Created

  • Set-Cookie جدید برای هر دو توکن

  • JSON: {“ok”:true}

جمع‌بندی

یک جریان کامل و مناسب تولید برای احراز هویت JWT در Go با Gin ساخته شد. با صدور access tokenهای کوتاه‌مدت، چرخش refresh tokenها، ذخیره امن آن‌ها در کوکی‌های HttpOnly و ابطال فوری با Redis، امنیت واقعی در یک اپ Go ایجاد می‌شود. همچنین middleware وجود دارد که هم از کوکی‌ها و هم از Bearer tokenها پشتیبانی می‌کند، به‌اضافه یک مکانیزم refresh برای زنده نگه داشتن سشن بدون اجبار به ورود مجدد.

با این پایه، می‌توان به‌سادگی کارهای زیر را انجام داد:

  • جایگزینی کاربر نمونه داخل حافظه با جست‌وجوی واقعی در پایگاه داده

  • پیاده‌سازی ردیابی refresh token به‌ازای هر دستگاه برای کنترل دقیق‌تر سشن‌ها

  • تغییر به امضای RS256 یا EdDSA برای اعتبارسنجی با کلید عمومی بین سرویس‌ها

  • ادغام با APIهای خارجی تا پس از برخی اکشن‌ها تماس یا پیام ارسال شود

JWT فقط برای احراز هویت نیست؛ یک بلوک سازنده برای سیستم‌های امن و بدون وضعیت است که قابلیت مقیاس‌پذیری دارند. با ترکیب آن با Redis برای ابطال و طراحی دقیق توکن، راهکاری ساخته می‌شود که بین کارایی، امنیت و بهره‌وری توسعه‌دهنده تعادل ایجاد می‌کند.

اگر این رویکرد در پروژه‌ای استفاده شود یا با احراز هویت چندمرحله‌ای، OAuth یا ادغام‌های API توسعه داده شود، امکان اشتراک تجربه و نتایج وجود دارد.

آنبوردینگ توسعه‌دهندگان (Developers) با کالکشن‌های API چگونه انجام می‌شود؟
کیت‌های آغازین Laravel 12 چیست و چگونه کار می‌کنند؟

دیدگاهتان را بنویسید

سبد خرید
علاقه‌مندی‌ها
مشاهدات اخیر
دسته بندی ها