Auth Service 101

บทความโดย มหาวิทยาลัยศิลปากรมหาวิทยาลัยศิลปากร

ภาควิชาคอมพิวเตอร์มหาวิทยาลัยศ

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์ และ อ.ดร.สัจจาภรณ์ ไวจรรยา
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

บทความนี้ใช้สำหรับสอนนักศึกษาเกี่ยวกับการทำ JWT Authentication และการทำ Authorization ใน Auth Service (REST API) พร้อมด้วยตัวอย่าง Code ภาษา Go เพื่อให้เข้าใจวิธีการตรวจสอบสิทธิ์และการป้องกันการเข้าถึงข้อมูลได้อย่างถูกต้อง

พื้นฐานของ Authentication และ Authorization

Authentication หรือ การตรวจสอบตัวตน เป็นกระบวนการที่ใช้ยืนยันตัวตนของผู้ใช้ เช่น การเข้าสู่ระบบด้วย Username และ Password

หลังจากที่ระบบตรวจสอบตัวตนแล้ว จะมีการกำหนดสิทธิ์การเข้าถึงทรัพยากร หรือ API ต่าง ๆ (Authorization) โดยอาจใช้แนวคิด Role-Based Access Control (RBAC) เพื่อกำหนดบทบาท (Role) ให้กับผู้ใช้และกำหนดสิทธิ์ (Permission) ที่สอดคล้องกับความรับผิดชอบของแต่ละบทบาท

RBAC เป็นแนวทางในการจัดการสิทธิ์ โดยแบ่งผู้ใช้ออกเป็นกลุ่ม (Role) และผูกสิทธิ์ (Permission) กับ Role นั้น ๆ เปรียบเสมือนการจัดการกุญแจเข้าห้องต่าง ๆ ในโรงเรียน โดยแต่ละคนจะมีกุญแจที่เปิดได้ไม่เหมือนกัน ขึ้นอยู่กับบทบาทของเขา

ตัวอย่างการใช้ RBAC ในระบบทะเบียนนักศึกษา โดยกำหนดบทบาท (Role) 3 แบบ ได้แก่

  • ผู้ใช้ที่มี Role เป็นผู้ดูแลระบบ (admin) สามารถจัดการรายวิชาได้ทั้งหมด และดูข้อมูลนักศึกษา
  • ผู้ใช้ที่มี Role เป็นอาจารย์ (teacher) สามารถดูรายวิชา ให้เกรด และดูข้อมูลนักศึกษา
  • ผู้ใช้ที่มี Role เป็นนักศึกษา (student) สามารถดูรายวิชาและเกรดของตนเอง

แต่ละ Role จะมีสิทธิ์ (Permission) ที่แตกต่างกัน โดย Permission ถูกกำหนดในรูปแบบ ใคร (admin, teacher และ student) สามารถทำ อะไร (create, update, delete และ view) กับ ทรัพยากรใด (course, student และ grade)

Casbin เป็น Library ใน go ที่รองรับการกำหนดและตรวจสอบสิทธิ์ตามโมเดลต่าง ๆ รวมถึง RBAC โดย Casbin จะใช้ File โมเดล (Model Configuration) และ File นโยบาย (Policy) เพื่อกำหนดว่าใคร (Subject) สามารถทำอะไร (Action) กับทรัพยากร (Object) ได้บ้าง

ตัวอย่าง RBAC ใน Casbin โดยมีโครงสร้างของ Project ดังต่อไปนี้

.
├── auth
├── go.mod
├── go.sum
├── main.go
├── model.conf
└── policy.csv
  1. model.conf

File นี้จะกำหนดรูปแบบของ request, policy, role definition, และ matchers ที่ใช้ในการตรวจสอบสิทธิ์

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
model.conf

[request_definition] คือ การกำหนดรูปแบบของข้อมูลที่ส่งเข้ามา (เช่น subject, object, action)

[policy_definition] คือ การกำหนดรูปแบบของนโยบาย (Policy) ว่า Role ใดสามารถกระทำอะไรกับทรัพยากรใด

[role_definition] คือ การกำหนดความสัมพันธ์ระหว่าง subject กับ role เช่น g, alice, admin

[matchers] คือ การกำหนดเงื่อนไขในการจับคู่สิทธิ์ ซึ่งในที่นี้จะเช็คว่า subject มี Role ที่ตรงกับนโยบายหรือไม่ และตรวจสอบว่า object กับ action ตรงกันหรือไม่

2. policy.csv

File นี้จะกำหนดนโยบาย (Policy) ว่า Role ใดสามารถทำอะไรกับทรัพยากรใดได้ตามที่ออกแบบไว้

# นโยบายสำหรับผู้ดูแลระบบ (admin)
p, admin, course, create
p, admin, course, update
p, admin, course, delete
p, admin, course, view
p, admin, student, view

# นโยบายสำหรับอาจารย์ (teacher)
p, teacher, course, view
p, teacher, grade, update
p, teacher, student, view
p, teacher, grade, view

# นโยบายสำหรับนักศึกษา (student)
p, student, course, view
p, student, grade, view

# กำหนดความสัมพันธ์ระหว่างผู้ใช้กับ Role
g, alice, admin
g, bob, teacher
g, charlie, student
policy.csv

3. ตัวอย่าง Code การใช้งาน Casbin

Code ตัวอย่างด้านล่างนี้แสดงวิธีการสร้าง Casbin enforcer และการตรวจสอบสิทธิ์ตามนโยบายในระบบทะเบียนนักศึกษา โดยมีรายละเอียดดังต่อไปนี้

สร้าง enforcer ด้วยการระบุไฟล์โมเดล (model.conf) และไฟล์นโยบาย (policy.csv)

ใช้ e.Enforce(sub, obj, act) ในการตรวจสอบสิทธิ์ เช่น e.Enforce("alice", "course", "create") จะตรวจสอบว่า alice (ซึ่งมี Role admin ตามที่กำหนดในไฟล์นโยบาย) มีสิทธิ์ในการสร้าง (create) รายวิชา (course) หรือไม่

ผลลัพธ์ที่ได้จะแสดงว่าผู้ใช้แต่ละคนสามารถกระทำ action กับทรัพยากรตามที่กำหนดได้หรือไม่

package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

func main() {
	// สร้าง Casbin enforcer โดยใช้ model.conf และ policy.csv
	e, err := casbin.NewEnforcer("model.conf", "policy.csv")
	if err != nil {
		log.Fatalf("Error creating enforcer: %v", err)
	}

	// ตัวอย่างตรวจสอบสิทธิ์

	// 1. ตรวจสอบว่า alice (admin) สามารถสร้างรายวิชาได้หรือไม่
	ok, err := e.Enforce("alice", "course", "create")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can alice create course? %v\n", ok) // ควรได้ true

	// 2. ตรวจสอบว่า bob (teacher) สามารถลบรายวิชาได้หรือไม่ (teacher ไม่มีสิทธิ์ลบรายวิชา)
	ok, err = e.Enforce("bob", "course", "delete")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can bob delete course? %v\n", ok) // ควรได้ false

	// 3. ตรวจสอบว่า bob (teacher) สามารถดูรายวิชาได้หรือไม่
	ok, err = e.Enforce("bob", "course", "view")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can bob view course? %v\n", ok) // ควรได้ true

	// 4. ตรวจสอบว่า charlie (student) สามารถดูเกรดของตนเองได้หรือไม่
	ok, err = e.Enforce("charlie", "grade", "view")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can charlie view grade? %v\n", ok) // ควรได้ true

	// 5. ตรวจสอบว่า charlie (student) สามารถแก้ไขเกรดได้หรือไม่
	ok, err = e.Enforce("charlie", "grade", "update")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can charlie update grade? %v\n", ok) // ควรได้ false
}
main.go

Casbin รองรับการที่ผู้ใช้ (user) สามารถมีหลาย role ได้ โดยใช้กลไกของ group definition (ส่วนที่ระบุด้วย g)

ปรับปรุง policy.csv ให้ผู้ใช้สามารถมีหลาย role ได้

# นโยบายสำหรับผู้ดูแลระบบ (admin)
p, admin, course, create
p, admin, course, update
p, admin, course, delete
p, admin, course, view
p, admin, student, view

# นโยบายสำหรับอาจารย์ (teacher)
p, teacher, course, view
p, teacher, grade, update
p, teacher, student, view
p, teacher, grade, view

# นโยบายสำหรับนักศึกษา (student)
p, student, course, view
p, student, grade, view

# กำหนดความสัมพันธ์ระหว่างผู้ใช้กับ Role
g, alice, admin
g, bob, teacher
g, charlie, student

# สมมุติว่า nuttachot สามารถมีหลาย role คือ teacher และ student
g, nuttachot, teacher
g, nuttachot, student
policy.csv

เมื่อมีการตรวจสอบสิทธิ์สำหรับ nuttachot ระบบจะตรวจสอบผ่านทั้งนโยบายที่กำหนดไว้สำหรับ teacher และ student

package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

func main() {
	// สร้าง Casbin enforcer โดยใช้ model.conf และ policy.csv
	e, err := casbin.NewEnforcer("model.conf", "policy.csv")
	if err != nil {
		log.Fatalf("Error creating enforcer: %v", err)
	}

	// ตรวจสอบสิทธิ์สำหรับ nuttachot ที่มี role teacher และ student

	// 1. ตรวจสอบว่า nuttachot สามารถดูรายวิชา (teacher: view) ได้หรือไม่
	ok, err := e.Enforce("nuttachot", "course", "view")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can nuttachot view course? %v\n", ok) // ควรได้ true (เพราะ teacher มีสิทธิ์ view course)

	// 2. ตรวจสอบว่า nuttachot สามารถให้เกรด (teacher: update grade) ได้หรือไม่
	ok, err = e.Enforce("nuttachot", "grade", "update")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can nuttachot update grade? %v\n", ok) // ควรได้ true (เพราะ teacher มีสิทธิ์ update grade)

	// 3. ตรวจสอบว่า nuttachot สามารถดูเกรด (student: view grade) ได้หรือไม่
	ok, err = e.Enforce("nuttachot", "grade", "view")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can nuttachot view grade? %v\n", ok) // ควรได้ true (เพราะ student มีสิทธิ์ view grade)

	// 4. ตรวจสอบว่า nuttachot สามารถลบรายวิชา (teacher/student ไม่มีสิทธิ์ delete) ได้หรือไม่
	ok, err = e.Enforce("nuttachot", "course", "delete")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can nuttachot delete course? %v\n", ok) // ควรได้ false
}
main.go

เราสามารถเพิ่มผู้ใช้ใหม่ที่มีหลาย role ได้ (ในตัวอย่างนี้คือ sajjaporn ที่มี role เป็นทั้ง teacher และ student)

เราจะใช้ Function e.AddGroupingPolicy(user, role)และ e.SavePolicy() เพื่อเพิ่มความสัมพันธ์ระหว่างผู้ใช้กับ role โดยมีเป้าหมาย คือ การเพิ่ม group definition (ส่วนที่ระบุด้วย g) ลงใน File policy.csv ดังตัวอย่างต่อไปนี้

g, sajjaporn, teacher
g, sajjaporn, student
policy.csv

ซึ่ง AddGroupingPolicy จะบันทึกการเปลี่ยนแปลงลงในหน่วยความจำ ส่วน e.SavePolicy() จะบันทึกการเปลี่ยนแปลงลงใน File policy.csv

package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

// ฟังก์ชันช่วยแสดงนโยบายทั้งหมด
func printPolicies(e *casbin.Enforcer) {
	policies, err := e.GetPolicy()
	if err != nil {
		fmt.Printf("Error retrieving policies: %v\n", err)
		return
	}
	groupingPolicies, err := e.GetGroupingPolicy()
	if err != nil {
		fmt.Printf("Error retrieving grouping policies: %v\n", err)
		return
	}

	fmt.Println("Direct Policies:")
	for _, p := range policies {
		fmt.Println(p)
	}
	fmt.Println("Grouping Policies:")
	for _, g := range groupingPolicies {
		fmt.Println(g)
	}
}

func main() {
	// โหลด enforcer จากไฟล์ model.conf และ policy.csv
	e, err := casbin.NewEnforcer("model.conf", "policy.csv")
	if err != nil {
		log.Fatalf("Error creating enforcer: %v", err)
	}

	// แสดงนโยบายก่อนเพิ่ม user ใหม่
	fmt.Println("Policies before adding new user:")
	printPolicies(e)

	// เพิ่ม sajjaporn ให้มี role เป็นทั้ง teacher และ student
	added, err := e.AddGroupingPolicy("sajjaporn", "teacher")
	if err != nil {
		log.Fatalf("Error adding grouping policy: %v", err)
	}
	if added {
		fmt.Println("Added grouping policy: (sajjaporn, teacher)")
	}
	added, err = e.AddGroupingPolicy("sajjaporn", "student")
	if err != nil {
		log.Fatalf("Error adding grouping policy: %v", err)
	}
	if added {
		fmt.Println("Added grouping policy: (sajjaporn, student)")
	}

	// บันทึกนโยบายกลับไปที่ไฟล์
	if err := e.SavePolicy(); err != nil {
		log.Fatalf("Error saving policy: %v", err)
	}

	// แสดงนโยบายหลังจากเพิ่ม user ใหม่
	fmt.Println("\nPolicies after adding new user:")
	printPolicies(e)

	// ทดสอบตรวจสอบสิทธิ์ของ sajjaporn
	// 1. ตรวจสอบว่า sajjaporn สามารถดูรายวิชา (action "view" บน "course") ได้หรือไม่
	ok, err := e.Enforce("sajjaporn", "course", "view")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("\nCan sajjaporn view course? %v\n", ok) // คาดหวัง true (เนื่องจาก teacher มีสิทธิ์ view course)

	// 2. ตรวจสอบว่า sajjaporn สามารถให้เกรด (action "update" บน "grade") ได้หรือไม่
	ok, err = e.Enforce("sajjaporn", "grade", "update")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can sajjaporn update grade? %v\n", ok) // คาดหวัง true (teacher มีสิทธิ์ update grade)

	// 3. ตรวจสอบว่า sajjaporn สามารถดูเกรด (action "view" บน "grade") ได้หรือไม่
	ok, err = e.Enforce("sajjaporn", "grade", "view")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can sajjaporn view grade? %v\n", ok) // คาดหวัง true (student มีสิทธิ์ view grade)

	// 4. ตรวจสอบว่า sajjaporn สามารถลบรายวิชา (action "delete" บน "course") ได้หรือไม่
	ok, err = e.Enforce("sajjaporn", "course", "delete")
	if err != nil {
		log.Fatalf("Enforce error: %v", err)
	}
	fmt.Printf("Can sajjaporn delete course? %v\n", ok) // คาดหวัง false (ไม่มี role ที่ให้สิทธิ์ delete course)
}
main.go

หมายเหตุ ในระบบ Production เราอาจใช้ฐานข้อมูลเพื่อจัดเก็บ Policy และจัดการสิทธิ์ผ่าน API แต่สำหรับตัวอย่างนี้เราใช้ File policy.csvเพื่อความง่ายในการสาธิต

พื้นฐานของ JSON Web Token (JWT)

JWT ประกอบด้วย 3 ส่วนหลัก ที่เข้ารหัสแบบ Base64 แล้วนำมาต่อกันด้วย . ได้แก่

  • Header สำหรับระบุประเภทของ Token (JWT) และ Algorithm ที่ใช้ลงนาม เช่น HS256
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload สำหรับบรรจุข้อมูล Claim เช่น User ID, Role เวลา Expiration (exp) เป็นต้น
{
  "exp": 1738894041,
  "username": "nuttachot"
}
  • Signature สำหรับการเข้ารหัสเพื่อให้แน่ใจว่า Token ไม่ถูกแก้ไข โดยใช้ Secret Key หรือในบางระบบอาจใช้ Private/Public Key
HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    Secret Key
)

ตัวอย่าง JWT 3 ส่วนที่นำมาต่อกันด้วย .

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzg4OTQwNDEsInVzZXJuYW1lIjoibnV0dGFjaG90In0.gdK2Dgxd7xo8JMvRP4uL64DscYam3tNKrvd7tK4hIMY

การใช้งานในระบบ Auth Service

  • เมื่อผู้ใช้ Login แล้ว ระบบจะออก JWT ให้กับผู้ใช้ โดย Token นี้จะถูกส่งกลับไปใน Response
  • เมื่อ Client ขอใช้งานระบบ JWT จะถูกส่งมายัง Backend กับ Request ใน Header (Authorization: Bearer <token>)
  • Server ที่รับ Request สามารถตรวจสอบ Token ด้วยการ Validate Signature และตรวจสอบการ Claim ต่าง ๆ (เช่น ตรวจสอบเวลาหมดอายุ)
  • เมื่อผ่านการตรวจสอบแล้ว จึงส่งข้อมูลกลับไปให้ Client
How JWT Authentication Work?

Code ด้านล่างต่อไปนี้เป็นตัวอย่างการใช้งาน JWT ในระบบ Auth Service โดยเมื่อผู้ใช้ Login สำเร็จ ระบบจะส่ง JWT กลับมา และเมื่อมีการเรียกใช้ Endpoint ที่ต้องการการ Authentication ผู้ใช้ต้องส่ง Token ที่ถูกต้องใน Header (Authorization: Bearer JWT) ไปยัง Backend เพื่อให้ระบบตรวจสอบความถูกต้องของ Token ก่อนให้เข้าถึงข้อมูล

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
)

// Secret key สำหรับใช้เข้ารหัส Token (ควรเก็บไว้เป็นความลับ)
var jwtSecret = []byte("your-secret-key")

// generateToken สร้าง JWT โดยรับ username เป็น claim พร้อมตั้งเวลาหมดอายุ (exp)
func generateToken(username string) (string, error) {
	// สร้าง token พร้อมกำหนด claims
	claims := jwt.MapClaims{
		"username": username,
		"exp":      time.Now().Add(72 * time.Hour).Unix(), // กำหนดอายุ token 72 ชั่วโมง
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// ลงนาม token ด้วย secret key
	tokenString, err := token.SignedString(jwtSecret)
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

// JWTAuthMiddleware เป็น middleware สำหรับตรวจสอบ JWT ใน Header ของ Request
func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// ดึงค่า Authorization header
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
			c.Abort()
			return
		}

		// คาดว่า header อยู่ในรูปแบบ "Bearer <token>"
		var tokenString string
		if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
			tokenString = authHeader[7:]
		} else {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
			c.Abort()
			return
		}

		// ตรวจสอบและแปลง token
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			// ตรวจสอบว่าใช้ Signing Method ที่คาดหวังหรือไม่
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
			}
			return jwtSecret, nil
		})
		if err != nil || !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
			c.Abort()
			return
		}

		// ดึงข้อมูล claims แล้วส่งต่อให้ handler ภายหลัง
		if claims, ok := token.Claims.(jwt.MapClaims); ok {
			c.Set("username", claims["username"])
		}
		c.Next()
	}
}

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

	// Endpoint สำหรับ Login: เมื่อ Login ผ่านระบบจะออก JWT ให้กับผู้ใช้
	router.POST("/login", func(c *gin.Context) {
		// ตัวอย่างข้อมูล Login (ในความเป็นจริงควรตรวจสอบกับฐานข้อมูล)
		var loginData struct {
			Username string `json:"username"`
			Password string `json:"password"`
		}
		if err := c.ShouldBindJSON(&loginData); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
			return
		}

		// ตรวจสอบความถูกต้องของ username และ password (ตัวอย่างนี้ใช้ค่า static)
		if loginData.Username != "nuttachot" || loginData.Password != "password" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
			return
		}

		// สร้าง JWT ให้กับผู้ใช้
		token, err := generateToken(loginData.Username)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
			return
		}

		// ส่ง Token กลับไปให้ Client
		c.JSON(http.StatusOK, gin.H{"token": token})
	})

	// สร้าง Group ของ Endpoint ที่ต้องการตรวจสอบ JWT
	protected := router.Group("/protected")
	protected.Use(JWTAuthMiddleware())
	{
		// Endpoint ตัวอย่างที่ต้องการ Authentication
		protected.GET("/profile", func(c *gin.Context) {
			// ดึง username จาก context ที่ถูกตั้งไว้ใน middleware
			username, _ := c.Get("username")
			c.JSON(http.StatusOK, gin.H{
				"message": fmt.Sprintf("Welcome %s! You have accessed a protected endpoint.", username),
			})
		})
	}

	// รัน Server ที่ port 8080
	router.Run(":8080")
}

ทดลอง Login และใช้งาน JWT ผ่าน Postman

  • เรียกใช้ REST API ด้วย Method POST ที่เส้น http://localhost:8080/login พร้อมส่ง JSON ดังต่อไปนี้
{
    "username": "nuttachot", 
    "password": "password"
}
  • หากข้อมูลถูกต้อง ระบบจะตอบกลับเป็น JSON ที่มี Token เช่น
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzg4OTAzMjIsInVzZXJuYW1lIjoibnV0dGFjaG90In0.0ywTRAdb5l1tuTefEteSgdleEd0suHGLkej6Z5-ZBaY"
}
  • ทดลองนำไป Decode ที่ https://jwt.io พร้อมใส่ Secret Key (your-secret-key) ของเราเอง
https://jwt.io
  • สร้าง Request ใหม่ด้วย Method GET ที่เส้น http://localhost:8080/protected/profile โดยไปที่ Tab Headers แล้วเพิ่ม header ดังต่อไปนี้
Key : Authorization
Value : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzg4OTAzMjIsInVzZXJuYW1lIjoibnV0dGFjaG90In0.0ywTRAdb5l1tuTefEteSgdleEd0suHGLkej6Z5-ZBaY

แนวทางการใช้งาน JWT สำหรับการทำ Authentication และ Casbin สำหรับการทำ Authorization (RBAC) ในระบบ Auth Service

  • เมื่อผู้ใช้ Login สำเร็จ ระบบจะสร้าง JWT ซึ่งภายใน Token จะมีข้อมูลอย่างเช่น username และเวลาหมดอายุ เมื่อผู้ใช้เรียก Endpoint ที่ต้องการการ Authentication จะต้องมีการส่ง JWT ใน Header (Authorization: Bearer ) มายัง Backend
  • หลังจากตรวจสอบ Valid และ Expired ของ JWT แล้ว เราจะใช้ Casbin ตรวจสอบสิทธิ์การเข้าถึง เช่น ผู้ใช้สามารถเข้าถึง Resource ใดและทำ Action อะไรได้บ้าง โดยในตัวอย่างนี้จะมีการกำหนดนโยบายใน File policy.csv และ Model ใน File  model.conf
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/casbin/casbin/v2"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
)

// Secret key สำหรับเข้ารหัส JWT (ควรเก็บไว้เป็นความลับ)
var jwtSecret = []byte("your-secret-key")

// generateToken สร้าง JWT โดยรับ username เป็น claim
func generateToken(username string) (string, error) {
	claims := jwt.MapClaims{
		"username": username,
		"exp":      time.Now().Add(72 * time.Hour).Unix(), // อายุ 72 ชั่วโมง
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

// JWTAuthMiddleware ตรวจสอบ JWT ใน Header ของ Request
func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// ดึง Authorization header
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
			c.Abort()
			return
		}

		// คาดว่า header อยู่ในรูปแบบ "Bearer <token>"
		var tokenString string
		if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
			tokenString = authHeader[7:]
		} else {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
			c.Abort()
			return
		}

		// ตรวจสอบและแปลง token
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			// ตรวจสอบ Signing Method
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
			}
			return jwtSecret, nil
		})
		if err != nil || !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
			c.Abort()
			return
		}

		// ดึง username จาก claims แล้วส่งต่อ
		if claims, ok := token.Claims.(jwt.MapClaims); ok {
			c.Set("username", claims["username"])
		}
		c.Next()
	}
}

func main() {
	// สร้าง Casbin enforcer โดยใช้ model.conf และ policy.csv
	enforcer, err := casbin.NewEnforcer("model.conf", "policy.csv")
	if err != nil {
		log.Fatalf("Error creating enforcer: %v", err)
	}

	router := gin.Default()

	// Endpoint สำหรับ Login เพื่อออก JWT
	router.POST("/login", func(c *gin.Context) {
		var loginData struct {
			Username string `json:"username"`
			Password string `json:"password"`
		}
		if err := c.ShouldBindJSON(&loginData); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
			return
		}

		// ในที่นี้ตรวจสอบข้อมูลแบบง่าย ๆ (ใน Production ควรตรวจสอบกับฐานข้อมูล)
		// ยอมรับผู้ใช้ alice, bob และ nuttachot
		if loginData.Username != "alice" && loginData.Username != "bob" && loginData.Username != "nuttachot" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
			return
		}

		token, err := generateToken(loginData.Username)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
			return
		}

		c.JSON(http.StatusOK, gin.H{"token": token})
	})

	// Group ของ endpoint ที่ต้องผ่าน JWT authentication
	protected := router.Group("/protected")
	protected.Use(JWTAuthMiddleware())
	{
		// Endpoint สำหรับตรวจสอบการเข้าถึง resource "grade" ด้วย action "read"
		protected.GET("/grade", func(c *gin.Context) {
			username, exists := c.Get("username")
			if !exists {
				c.JSON(http.StatusUnauthorized, gin.H{"error": "User information not found"})
				return
			}

			// ตรวจสอบสิทธิ์สำหรับ resource "grade" ด้วย action "read"
			allowed, err := enforcer.Enforce(username, "grade", "view")
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "Error occurred during authorization"})
				return
			}
			if !allowed {
				c.JSON(http.StatusForbidden, gin.H{"error": "Access denied: You don't have permission to read grade"})
				return
			}

			c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Hello %s, you have access to read grade", username)})
		})
	}

	// รัน Server ที่ port 8080
	router.Run(":8080")
}
main.go

ทดลอง Login และใช้งาน JWT ผ่านคำสั่ง Curl

curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "nuttachot", "password": "password"}' 
curl -X GET http://localhost:8080/protected/grade \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzg4OTQwNDEsInVzZXJuYW1lIjoibnV0dGFjaG90In0.gdK2Dgxd7xo8JMvRP4uL64DscYam3tNKrvd7tK4hIMY"
ทั้งนี้เพื่อนำ Code นี้ไปใช้ในระบบจริง เราควรจัดโครงสร้าง Code ใหม่ให้แยกเป็น Package ต่าง ๆ โดยมีการทดสอบที่ครอบคลุม การจัดการ Error ที่เหมาะสม และมาตรการด้านความปลอดภัย นอกจากนี้ควรเพิ่มระบบ Logging สำหรับติดตามการทำงานด้วย