การยืนยันตัวตนด้วย HMAC-SHA256 (HMAC Authentication)
กระบวนการ Authentication ของ RawPush ออกแบบมาเพื่อความปลอดภัยสูงสุด โดยใช้การสร้าง Signature จาก Backend ของคุณด้วยอัลกอริทึม HMAC-SHA256 เพื่อยืนยันว่า Client ที่กำลังจะ connect WebSocket ได้รับอนุญาตจาก Backend ของคุณจริง
🔒 1. ทำไมถึงใช้ HMAC-SHA256?
- ความปลอดภัยสูง: Secret Key ไม่ต้องถูกส่งข้าม Network แม้แต่ครั้งเดียว สิ่งที่ส่งคือ Signature ที่คำนวณจากข้อมูลผ่าน Secret Key เท่านั้น
- ป้องกันการปลอมแปลง (Tamper-proof): หากมีใครแก้ไขข้อมูลใน Token ค่า Signature จะไม่ตรงกัน และระบบจะปฏิเสธทันที
- ป้องกัน Replay Attack: ด้วยการแนบ ID ไม่ซ้ำ (JTI) ใน Payload และให้ RawPush เก็บไว้ แม้จะมีคนดักจับ Token เก่าไป ก็ไม่สามารถนำกลับมาใช้ซ้ำได้
🛠️ 2. ขั้นตอนการสร้าง Token ที่ฝั่ง Backend ของคุณ
เป้าหมายคือการสร้าง Token ส่งกลับไปให้ Client ที่ผ่านการ Login แล้ว เพื่อนำไปใช้ authenticate กับ WebSocket
โครงสร้างของ Token (Data Payload)
โครงสร้าง Token Payload (JSON) ที่ใช้สำหรับสร้าง Signature:
{
"v": "v1",
"purpose": "ws-auth",
"sub": "usr_xxxxxx",
"aud": "websocket",
"iat": 1718000000000,
"exp": 1718000600000,
"jti": "random_unique_uuid"
}v: เวอร์ชันของ Token (ใช้"v1") - เผื่อรองรับการอัปเกรดโครงสร้างในอนาคตpurpose: จุดประสงค์การใช้งาน (ใช้"ws-auth"เป็นมาตรฐาน)sub: ย่อมาจาก Subject คือ User ID ของผู้ใช้ในระบบของคุณaud: ย่อมาจาก Audience (ใช้"websocket"เป็นมาตรฐาน)iat: ย่อมาจาก Issued At (Unix Timestamp เป็น มิลลิวินาที) คือเวลาที่สร้าง Tokenexp: ย่อมาจาก Expiration (Unix Timestamp เป็น มิลลิวินาที) คือเวลาหมดอายุ แนะนำให้บวก 30-60 วินาทีjti: ย่อมาจาก JWT ID (Json Token ID) คือตัวอักษรสุ่มที่ไม่ซ้ำกัน (เช่น UUID) เพื่อป้องกัน Replay Attack แบบเจาะจง
ทำไมถึงแนะนำให้ Fix ค่า purpose และ aud ?
ทางเทคนิคแล้ว RawPush ไม่ได้บังคับ ว่าสองฟิลด์นี้จะต้องเป็นคำว่าอะไร (คุณจะใช้ชื่อแอปของคุณก็ได้) ระบบขอเพียงแค่ ส่งค่าเดิมให้ตรงกันทั้งฝั่ง Backend และ Client
แต่ในเชิง Security (Best Practice) หาก Backend ของคุณมีการใช้ Secret Key ตัวเดียวกันนี้ในการสร้าง Token สำหรับบริการอื่นๆ ด้วย การฮาร์ดโค้ด 2 ฟิลด์นี้ไว้ จะช่วยป้องกันการโจมตีข้ามระบบ (Cross-Service Replay Attack) ได้อย่างเด็ดขาด:
- ถ้านำ Token ของระบบแชท ไปยิงใส่ระบบไฟแนนซ์: จะพังทันทีเพราะค่า
audไม่ตรง - ถ้านำ Token ระบบใดๆ มายิงใส่ WebSocket: Gateway ของ RawPush จะปฏิเสธทันทีเพราะ
purposeหรือ Payload ภายในไม่ตรง ทำให้ Signature Hash ผิดพลาด
💻 3. ตัวอย่างโค้ดสร้าง Signature (Backend Code)
นำค่าใน Payload มาต่อกันเป็น string 7 บรรทัด แล้ว sign ด้วย Secret Key (sk_...) ของคุณ
วิธีการต่อ String คั่นด้วย newline (\n) ในลำดับดังนี้: v\npurpose\nsub\naud\niat\nexp\njti
ตัวอย่าง: Node.js / TypeScript
import crypto from 'crypto';
// กำหนดตัวแปรสำคัญ
const secretKey = 'sk_your_secret_key'; // เก็บเป็น Environment variable เท่านั้น!
const userId = 'usr_123456';
// 1. กำหนดเวลาเป็น มิลลิวินาที
const nowMs = Date.now();
const expMs = nowMs + (10 * 60 * 1000); // หมดอายุใน 10 นาที
// 2. ข้อมูล Token Payload
const tokenPayload = {
v: "v1",
purpose: "ws-auth",
sub: userId,
aud: "websocket",
iat: nowMs,
exp: expMs,
jti: crypto.randomUUID()
};
// 3. เรียงข้อมูลก่อน sign (ใช้ newline \n คั่น)
const payloadString = `${tokenPayload.v}\n${tokenPayload.purpose}\n${tokenPayload.sub}\n${tokenPayload.aud}\n${tokenPayload.iat}\n${tokenPayload.exp}\n${tokenPayload.jti}`;
// 4. สร้าง Signature (HMAC-SHA256) และ encode เป็น HEX
const signature = crypto
.createHmac('sha256', secretKey)
.update(payloadString)
.digest('hex');
// 5. ส่งค่านี้กลับไปให้ Frontend (Client) นำไปใช้งาน
const tokenForFrontend = {
token: tokenPayload,
signature: signature
};
// res.json(tokenForFrontend);ตัวอย่าง: Python
import hmac
import hashlib
import time
import uuid
secret_key = 'sk_your_secret_key'
user_id = 'usr_123456'
# 1. กำหนดเวลาเป็น มิลลิวินาที
now_ms = int(time.time() * 1000)
exp_ms = now_ms + (10 * 60 * 1000)
token_payload = {
"v": "v1",
"purpose": "ws-auth",
"sub": user_id,
"aud": "websocket",
"iat": now_ms,
"exp": exp_ms,
"jti": str(uuid.uuid4())
}
# 2. เรียงข้อมูลก่อน sign (Newline delimited)
payload_string = f"{token_payload['v']}\n{token_payload['purpose']}\n{token_payload['sub']}\n{token_payload['aud']}\n{token_payload['iat']}\n{token_payload['exp']}\n{token_payload['jti']}"
# 3: สร้าง Signature (HMAC-SHA256 Hex)
signature = hmac.new(
secret_key.encode('utf-8'),
msg=payload_string.encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
print({
"token": token_payload,
"signature": signature
})ตัวอย่าง: Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/google/uuid"
)
func generateToken(userID, secretKey string) map[string]interface{} {
nowMs := time.Now().UnixMilli()
expMs := nowMs + (10 * 60 * 1000) // 10 นาที
jti := uuid.New().String()
token := map[string]interface{}{
"v": "v1",
"purpose": "ws-auth",
"sub": userID,
"aud": "websocket",
"iat": nowMs,
"exp": expMs,
"jti": jti,
}
payloadString := fmt.Sprintf("%s\n%s\n%s\n%s\n%d\n%d\n%s",
token["v"], token["purpose"], token["sub"], token["aud"], token["iat"], token["exp"], token["jti"])
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(payloadString))
signature := hex.EncodeToString(h.Sum(nil))
return map[string]interface{}{
"token": token,
"signature": signature,
}
}🚀 4. นำ Token ไปให้ Client ใช้งาน
เมื่อ Backend ส่ง token และ signature กลับมาให้ Frontend แล้ว Client แค่นำข้อมูล 2 ส่วนนี้แนบไปกับ Command auth หลัง connect WebSocket เป็นอันเสร็จ
ตัวอย่างใน Frontend (JavaScript):
ws.onopen = () => {
ws.send(JSON.stringify({
cmd: "auth",
token: {
v: "v1",
purpose: "ws-auth",
sub: "usr_123456",
aud: "websocket",
iat: 1718000000000,
exp: 1718000600000,
jti: "random_uuid"
},
signature: "d83c2a6f...." // Signature (HEX 64 หลัก) จาก Backend
}));
};