İçerikler

Golang Redis ile JWT Kimlik Doğrulaması

Golang Redis ile JWT Kimlik Doğrulaması

Bu makalede, Golang kullanarak JWT tabanlı bir kimlik doğrulama sistemi oluşturacağız. Sistemimiz şu teknolojileri kullanacak:

  • Fiber: Hızlı bir web framework
  • GORM: PostgreSQL ile etkileşim için ORM
  • JWT: Kimlik doğrulama için JSON Web Token
  • Redis: Token yönetimi için önbellek
  • PostgreSQL: Kullanıcı verilerini depolamak için veritabanı
  • Bcrypt: Şifre hashleme için güvenli algoritma

Yukarıda yazdıklarımız Golang Projesi içerisinde kullanacakalarımız bunlara ek olarak proje dışı bileşenler olarak

  • Redis: Token yönetimi için önbellek
  • PostgreSQL: Kullanıcı verilerini depolamak için veritabanı

Gereksinimler

Bu projeyi gerçekleştirmek için aşağıdaki araçlara ihtiyacınız olacak:

  1. Go: En son sürümü https://golang.org/dl/ adresinden indirebilirsiniz.

  2. PostgreSQL:

    PostgreSQL kurulumundan sonra:

    • Yeni bir veritabanı oluşturun (örneğin: auth_system)
    • Kullanıcı adı, şifre ve veritabanı adını not edin (.env dosyasında kullanacağız)
  3. Redis:

    Redis kurulumundan sonra:

    • Varsayılan olarak localhost:6379 adresinde çalışacaktır
    • RedisInsight ile bağlanarak verileri görselleştirebilirsiniz
  4. IDE:

  5. Postman: API’nizi test etmek için (https://www.postman.com/)

Proje Oluşturma

  • Öncelikle, proje için yeni bir dizin oluşturun ve Go modülünü başlatın:
1
2
3
mkdir jwt-auth
cd jwt-auth
go mod init jwt-auth
  • Gerekli bağımlılıkları yükleyin:
1
2
3
4
5
6
7
8

go get -u github.com/gofiber/fiber/v2
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/golang-jwt/jwt/v4
go get -u github.com/go-redis/redis/v8
go get -u golang.org/x/crypto/bcrypt
go get -u github.com/joho/godotenv

Proje Yapısı

Projemizin yapısı şu şekilde olacak:

jwt-auth/
│
├── config/
│   └── config.go
├── database/
│   └── database.go
├── handlers/
│   └── auth.go
├── middleware/
│   └── auth.go
├── models/
│   └── user.go
├── .env
└── main.go

Env Dosyamız

  • ‘.env’ dosyasını oluşturun ve aşağıdaki içerikleri kendi veritabanı bilgilerinizle doldurun:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

DB_HOST=localhost
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=your_db_name
DB_PORT=5432
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
JWT_SECRET=your_jwt_secret

Konfigürasyon

  • ‘config’ dizini altında ‘config.go’ dosyasını oluşturun:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package config

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

type Config struct {
    DBHost        string
    DBUser        string
    DBPassword    string
    DBName        string
    DBPort        string
    RedisAddr     string
    RedisPassword string
    RedisDB       int
    JWTSecret     string
}

func LoadConfig() Config {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf("Error loading .env file")
    }

    return Config{
        DBHost:     os.Getenv("DB_HOST"),
        DBUser:     os.Getenv("DB_USER"),
        DBPassword: os.Getenv("DB_PASSWORD"),
        DBName:     os.Getenv("DB_NAME"),
        DBPort:     os.Getenv("DB_PORT"),
        RedisAddr:  os.Getenv("REDIS_ADDR"),
        RedisPassword: os.Getenv("REDIS_PASSWORD"),
        RedisDB:    os.Getenv("REDIS_DB"),
        JWTSecret:  os.Getenv("JWT_SECRET"),
    }
}

Veritabanı Bağlantısı

  • ‘database’ dizini altında ‘database.go’ dosyasını oluşturun:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package database

import (
	"fmt"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"jwt-auth/config"
	"log"
)

var DB *gorm.DB

func Connect(cfg config.Config) {
	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
		cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort)

	var err error
	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("Veritabanına bağlanılamadı!", err)
	}
}

Modeller

  • ‘models’ dizini altında ‘user.go’ dosyasını oluşturun:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package models

import (
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Username string `gorm:"unique"`
	Password string
}

func Migrate(db *gorm.DB) {
	db.AutoMigrate(&User{})
}

Handler’lar

  • ‘handlers’ dizini altında ‘auth.go’ dosyasını oluşturun:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package handlers

import (
	"context"
	"github.com/go-redis/redis/v8"
	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
	"jwt-auth/config"
	"jwt-auth/database"
	"jwt-auth/models"
	"strconv"
	"time"
)

var ctx = context.Background()

func Register(c *fiber.Ctx) error {
	var data map[string]string

	if err := c.BodyParser(&data); err != nil {
		return err
	}

	password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)

	user := models.User{
		Username: data["username"],
		Password: string(password),
	}

	database.DB.Create(&user)

	return c.JSON(user)
}

func Login(c *fiber.Ctx) error {
	var data map[string]string

	if err := c.BodyParser(&data); err != nil {
		return err
	}

	var user models.User
	database.DB.Where("username = ?", data["username"]).First(&user)

	if user.ID == 0 {
		c.Status(fiber.StatusNotFound)
		return c.JSON(fiber.Map{
			"message": "Usern Not Found",
		})
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(data["password"])); err != nil {
		c.Status(fiber.StatusBadRequest)
		return c.JSON(fiber.Map{
			"message": "wrong password",
		})
	}

	claims := jwt.MapClaims{
		"user_id": user.ID,
		"exp":     time.Now().Add(time.Hour * 24).Unix(), // 1 gün geçerlilik süresi
	}

	// Create access token
	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	config := config.LoadConfig()
	accessTokenString, err := accessToken.SignedString([]byte(config.JWTSecret))
	if err != nil {
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	// Create refresh token
	refreshClaims := jwt.MapClaims{
		"user_id": user.ID,
		"exp":     time.Now().Add(time.Hour * 24 * 7).Unix(), // 1 hafta geçerlilik süresi
	}
	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
	refreshTokenString, err := refreshToken.SignedString([]byte(config.JWTSecret))
	if err != nil {
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	// Store tokens in Redis
	rdb := redis.NewClient(&redis.Options{
		Addr:     config.RedisAddr,
		Password: config.RedisPassword,
		DB:       config.RedisDB,
	})

	rdb.Set(ctx, strconv.Itoa(int(user.ID)), accessTokenString, time.Hour*24)
	rdb.Set(ctx, "refresh_"+strconv.Itoa(int(user.ID)), refreshTokenString, time.Hour*24*7)

	return c.JSON(fiber.Map{
		"access_token":  accessTokenString,
		"refresh_token": refreshTokenString,
	})
}

func User(c *fiber.Ctx) error {
	user := c.Locals("user").(models.User)
	return c.JSON(user)
}

func Logout(c *fiber.Ctx) error {
	user := c.Locals("user").(models.User)
	config := config.LoadConfig()
	rdb := redis.NewClient(&redis.Options{
		Addr:     config.RedisAddr,
		Password: config.RedisPassword,
		DB:       config.RedisDB,
	})

	rdb.Del(ctx, strconv.Itoa(int(user.ID)))
	rdb.Del(ctx, "refresh_"+strconv.Itoa(int(user.ID)))

	return c.JSON(fiber.Map{
		"message": "Successful",
	})
}

func Refresh(c *fiber.Ctx) error {
	user := c.Locals("user").(models.User)

	// Generate new access token
	claims := jwt.MapClaims{
		"user_id": user.ID,
		"exp":     time.Now().Add(time.Hour * 24).Unix(), // 1 gün geçerlilik süresi
	}
	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	config := config.LoadConfig()
	accessTokenString, err := accessToken.SignedString([]byte(config.JWTSecret))
	if err != nil {
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	// Generate new refresh token
	refreshClaims := jwt.MapClaims{
		"user_id": user.ID,
		"exp":     time.Now().Add(time.Hour * 24 * 7).Unix(), // 1 hafta geçerlilik süresi
	}
	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
	refreshTokenString, err := refreshToken.SignedString([]byte(config.JWTSecret))
	if err != nil {
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	// Store new tokens in Redis
	rdb := redis.NewClient(&redis.Options{
		Addr:     config.RedisAddr,
		Password: config.RedisPassword,
		DB:       config.RedisDB,
	})

	rdb.Set(ctx, strconv.Itoa(int(user.ID)), accessTokenString, time.Hour*24)
	rdb.Set(ctx, "refresh_"+strconv.Itoa(int(user.ID)), refreshTokenString, time.Hour*24*7)

	return c.JSON(fiber.Map{
		"access_token":  accessTokenString,
		"refresh_token": refreshTokenString,
	})
}

Middleware

  • ‘middleware’ dizini altında ‘auth.go’ dosyasını oluşturun:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package middleware

import (
	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt/v4"
	"jwt-auth/config"
	"jwt-auth/database"
	"jwt-auth/models"
	"strings"
)

func Auth(c *fiber.Ctx) error {
	config := config.LoadConfig()
	authHeader := c.Get("Authorization")
	if authHeader == "" {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "No authorization header"})
	}

	// Token'ı "Bearer " kısmını kaldırarak ayıklayın
	tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fiber.ErrUnauthorized
		}
		return []byte(config.JWTSecret), nil
	})

	if err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Invalid token"})
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if ok && token.Valid {
		userID, ok := claims["user_id"].(float64)
		if !ok {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Invalid token claims"})
		}

		var user models.User
		database.DB.First(&user, uint(userID))

		if user.ID == 0 {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "User not found"})
		}

		c.Locals("user", user)
		return c.Next()
	} else {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Invalid token"})
	}
}

Ana Dosya

  • Son olarak, ‘main.go’ dosyasını oluşturun:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"github.com/gofiber/fiber/v2"
	"jwt-auth/config"
	"jwt-auth/database"
	"jwt-auth/handlers"
	"jwt-auth/middleware"
	"jwt-auth/models"
)

func main() {
	cfg := config.LoadConfig()

	app := fiber.New()

	database.Connect(cfg)
	models.Migrate(database.DB)

	app.Post("/register", handlers.Register)
	app.Post("/login", handlers.Login)

	app.Use(middleware.Auth)
	app.Get("/user", handlers.User)
	app.Post("/logout", handlers.Logout)
	app.Post("/refresh", handlers.Refresh)

	app.Listen(":6000")
}

Uygulamayı Çalıştırma

  • PostgreSQL ve Redis veritabanlarının çalıştığından emin olun.
  • Ortamınızı ayarladıktan sonra, uygulamanızı şu komutla çalıştırabilirsiniz:
1
go run main.go

uygulamyı çalıştırıyoruz
*Şekil 1: Uygulamanın çalışması *
- Uygulamanız http://localhost:6000 adresinde çalışıyor olmalıdır ve Postman veya cURL gibi araçları kullanarak uç noktaları test edebilirsiniz.

Uygulamayı Test Etme

  • Uygulamımızı ben postman Kullanarak test ettim sizde dilerseniz postman ile testlerinizi yapabilirsiniz.

Kullanıcı Oluşturma

  • Postman ile post gönderimi yapacağız adres kısmına http://localhost:6000/register yazıyoruz ve Body kısmını raw seçip aşağıdaki json’ı yazıyoruz siz dilediğiniz kullanıcı adı ve şifreyle yapabilrisiniz
1
2
3
4
{
    "username": "superuser",
    "password": "su12345"
}

Postman Kullanıcı oluşturma
*Şekil 2: Postman Kullanıcı oluşturma*
  • Postman ile gönderdiğimiz register post’una aldığımız cevap ta aşağıdaki gibi
Postman Kullanıcı oluşturma Dönüş mesajı
*Şekil 3: Postman Kullanıcı oluşturma Post Dönüşü *
  • Veritabanında oluşan kayıt aşağıdaki gibi
Vertitabanında oluşan kayıt
*Şekil 4: Vertitabanın da oluşan kayıt *

Kullanıcı Girişi

  • Postman ile post gönderimi yapacağız adres kısmına http://localhost:6000/login yazıyoruz ve Body kısmını raw seçip aşağıdaki json kısmına oluşturduğumuz kullanıcı adı ve şifreyi yazıyoruz.
Postman İle Kullanıcı girişi
*Şekil 5: Postman İle Kullanıcı girişi *
- Şekilde görüldüğü gibi giriş işlemi başarıyla tamamlandı, bize acces_token ve refresh_token olmak üzere iki adet jwt token oluşturuldu, bu tokenlar redis veritabınımızda tutuluyor onu da aşağıdaki resimde görebilirsiniz. acces_token'ı bir yere not alalım çünkü bir sonraki adımda kullanmamız gerekecek.

Redis te Tutlan token kayıtları
*Şekil 6: Rediste Tutlan token kayıtları *

Kullanıcı Detaylarını Alma

  • Postman ile get işlemi yapacağız adres kısmına http://localhost:6000/user yazıyoruz ve Authentication kısmında Auth Type’ı Bearer Token seçip token kısmına not aldığım acces_token’ı yazıyoruz.

Kullanıcı Detayları
*Şekil 7: Kullanıcı Detayları *

Token Yenileme

  • Postman ile post gönderimi yapacağız adres kısmına http://localhost:6000/user yazıyoruz başka birşey yapmadan send butonuna basabiliriz.
Refresh Token
*Şekil 8: Token Yenileme *

Çıkış Yapma

  • Postman ile post gönderimi yapacağız adres kısmına http://localhost:6000/logout yazıyoruz başka birşey yapmadan send butonuna basabiliriz.
Logout
*Şekil 8: Çıkış yapma *

Sonuç

  • Bu makalede, Golang, Fiber, GORM, PostgreSQL ve Redis kullanarak JWT tabanlı bir kimlik doğrulama sistemi oluşturmayı adım adım anlatmaya çalıştım. Artık ihtiyacınıza göre ek özellikler ekleyebileceğiniz temel bir çerçeveye sahipsiniz.

  • Bu makalanin projelerinizi geliştirirken faydalı olmasını umuyorum. Sorularınız veya eklemelenmesi gereken noktalar varsa, yardımcı olmaktan memnuniyet duyarım!

  • Bir sonraki yazılarımda bu projenin Frontend kısmınıda Svelte kullanarak yazıp paylaşıyor olacağım.

  • Projenin kaynak kodlarına GitHub’dan ulaşabilirsiniz.Github