ทำระบบ auth ด้วย Go Fiber

มาถึงปัจจุบันถ้าในวงการไทยสาย Backend หรือ Devops ผมกล้าพูดเลยว่าไม่มีใครไม่รู้จัก Golang ด้วยความที่เป็นภาษาที่ถูกคิดค้นด้วยทีมพัฒนาจาก Google และประสิทธิภาพการทำงานที่มีคนเปรียบเทียบแล้วว่าดีกว่าหลายๆภาษาใน ปัจจุบัน อาทิเช่น Nodejs ซึ่งเป็นที่นิยมมากในหมู่ developer วันนี้ผมจึงมาสอนการเขียนระบบอะไรง่ายของ Golang เช่น Auth System โดย Project Structure ตอนจบจะเป็นประมาณนี้

├── Readme.md
├── controller
│   └── authController.go
├── database
│   └── connect.go
├── dev.env
├── go.mod
├── go.sum
├── main.go
├── models
│   └── user.go
└── routes
    └── routes.go

ก่อนจะเริ่ม serve ระบบให้เรามานั่งคิดก่อนว่าระบบนี้ควรจะใช้ model อะไรบ้างเพื่อง่ายต่อการ implement เนื่องจากผมทำระบบ auth ยังไงก็ต้องเกี่ยวกับ User ดังนั้น…

// models/user.go
package models

type User struct {
	Id uint
	FirstName string
	LastName string
	Email string `gorm:unique`
	Password []byte
}
// database.connect.go
package database

import (
	"demo/models"
	"github.com/joho/godotenv"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"log"
	"os"
)
// function ดึงตัวแปรที่อยู่ในไฟล์ที่ชื่อ dev.env มาใช้ผ่าน parameter ที่เรียก
func envFetcher(key string) string {
	err := godotenv.Load("dev.env")
	if err != nil {
		log.Fatalf("Error Cannot load env file")
	}
	return os.Getenv(key)
}
// declare instance variable ของ gorm
var DB *gorm.DB

func Connect() {
	dns := "host=" + envFetcher("DB_HOST") + " " + "user=" + envFetcher("DB_USERNAME") + " " + "password=" + envFetcher("DB_PASSWORD") + " " + "dbname=" + envFetcher("DB_NAME")

	// ทำการเปิด connection โดยใช้ gorm.open ผ่าน postgres database โดยที่อยู่ ของ database นั้น ถูกระบุไว้ในตัวแปร dns
	connection, err := gorm.Open(postgres.Open(dns), &gorm.Config{})

	// ถ้ามี error ในระหว่างการ connect ก็จะให้ panic message Could not connect to database ⚠️
	if err != nil {
		panic("Could not connect to database ⚠️")
	}
	// ถ้าไม่มี error ก็ให้ทำการ migrate model user ที่เราเขียนไว้เพื่อมาเป็น table ใน database
	if err := connection.AutoMigrate(&models.User{}); err != nil {
		panic("Cannot migrate to user table ⚠️")
	}
}
// routes/routes.go
package routes

import (
	"demo/controller"
	"github.com/gofiber/fiber/v2"
)

func Setup(app *fiber.App) {
	app.Post("/api/register", controller.Register)
	app.Post("/api/login", controller.Login)
	app.Post("/api/logout", controller.Logout)

	app.Get("/api/user", controller.User)
}
// controller/authController.go
package controller

import (
	"demo/database"
	"demo/models"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"github.com/gofiber/fiber/v2"
	"golang.org/x/crypto/bcrypt"
	"strconv"
	"time"
)

type Claims struct {
	jwt.StandardClaims
}

func Register(c *fiber.Ctx) error {
	var data map[string]string
	// แปลง context data ที่รับมาใส่ตัวแปร data โดยให้อยู่ในรูป json object
	if err := c.BodyParser(&data); err != nil {
		return err
	}
	// เช็ค password กับ confirm_password ที่รับเข้ามา
	if data["password"] != data["confirm_password"] {
		c.Status(400)
		return c.JSON(fiber.Map{
			"message": "Password doesn't match",
		})
	}
	encodedPassword, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
	user := models.User{
		FirstName: data["first_name"],
		LastName:  data["last_name"],
		Email:     data["email"],
		Password:  encodedPassword,
	}
	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("email = ?", data["email"]).First(&user)
	if user.Id == 0 {
		c.Status(404)
		return c.JSON(fiber.Map{
			"message": "User Not found",
		})
	}
	if err := bcrypt.CompareHashAndPassword(user.Password, []byte(data["password"])); err != nil {
		return c.JSON(fiber.Map{
			"message": "email or password incorrect",
		})
	}
	claims := jwt.StandardClaims{
		Issuer:    strconv.Itoa(int(user.Id)),
		ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
	}
	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	token, err := jwtToken.SignedString([]byte("secret"))
	if err != nil {
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	cookie := fiber.Cookie{
		Name:     "jwt",
		Value:    token,
		Expires:  time.Now().Add(time.Hour * 24),
		HTTPOnly: true,
	}
	c.Cookie(&cookie)
	return c.JSON(fiber.Map{
		"jwt": token,
	})
}

func User(c *fiber.Ctx) error {
	cookie := c.Cookies("jwt")

	token, err := jwt.ParseWithClaims(cookie, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte("secret"), nil
	})
	if err != nil || !token.Valid {
		c.Status(fiber.StatusUnauthorized)
		return c.JSON(fiber.Map{
			"message": "unauthenticated",
		})
	}
	claims := token.Claims.(*Claims)
	var user models.User
	database.DB.Where("id = ?", claims.Issuer).First(&user)
	fmt.Print(claims.Issuer)
	return c.JSON(user)
}

func Logout(c *fiber.Ctx) error {
	cookie := fiber.Cookie{
		Name:     "jwt",
		Value:    "",
		Expires:  time.Now().Add(-time.Hour),
		HTTPOnly: true,
	}
	c.Cookie(&cookie)
	return c.JSON(fiber.Map{
		"message": "logout success!",
	})

}

Serve ระบบด้วย Fiber

package main

import (
	"demo/database"
	"demo/routes"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
)

func main() {
	database.Connect() // --> ทำการต่อ Database ด้วย ORM
	app := fiber.New() // --> new Fiber named instance
	app.Use(cors.New(cors.Config{ // --> บอก Server ให้ Allow หน้าบ้านให้แนบ Cookie มากับ Req header ได้
		AllowCredentials: true,
	}))
	routes.Setup(app) // --> setup routes โดยการส่ง fiber context
	app.Listen(":3000") // -->  Listen serves HTTP requests th port 3000
}
Share This:

ระบบติดตามข่าวสาร Coming soon นะครับ 😅