Now.js Framework Documentation
Authentication System - ระบบการตรวจสอบสิทธิ์
Authentication System - ระบบการตรวจสอบสิทธิ์
เอกสารฉบับนี้สรุปภาพรวมสถาปัตยกรรม ฟีเจอร์ และขั้นตอนการใช้งานระบบการตรวจสอบสิทธิ์ (Authentication) ของ Now.js Framework พร้อมแนวทางการออกแบบและการรักษาความปลอดภัยที่ควรรู้
📋 สารบัญ
- ภาพรวม
- สถาปัตยกรรมระบบ
- คอมโพเนนต์หลัก
- เริ่มต้นใช้งานอย่างรวดเร็ว
- ลำดับขั้นการยืนยันตัวตน
- ตัวอย่างการใช้งาน
- แนวทางปฏิบัติที่แนะนำ
- ข้อควรคำนึงด้านความปลอดภัย
- เอกสารที่เกี่ยวข้อง
ภาพรวม
ระบบการตรวจสอบสิทธิ์ของ Now.js ถูกออกแบบให้ครอบคลุมวงจรการยืนยันตัวตนทั้งหมด ทั้งในมุมมองของผู้ใช้ ฝั่งไคลเอนต์ และการประสานกับ API เพื่อให้ทีมพัฒนาสามารถสร้างเว็บแอปพลิเคชันสมัยใหม่ได้อย่างปลอดภัยและยืดหยุ่น
ฟีเจอร์หลัก
- ✅ รองรับ JWT แบบครบถ้วน: จัดการทั้ง access token และ refresh token ในตัว
- ✅ หลายกลยุทธ์การยืนยันตัวตน: ใช้งาน Bearer, Basic, OAuth หรือโหมดผสมได้ตามต้องการ
- ✅ คุกกี้ HttpOnly: เก็บโทเค็นในคุกกี้ HttpOnly เพื่อป้องกันการถูกขโมยผ่าน XSS
- ✅ รีเฟรชโทเค็นอัตโนมัติ: ต่ออายุโทเค็นก่อนหมดอายุเพื่อรักษาประสบการณ์ใช้งาน
- ✅ Route Guard อัจฉริยะ: ตรวจสอบบทบาท (role) และสิทธิ์ (permission) ก่อนเข้าแต่ละเส้นทาง
- ✅ ป้องกัน CSRF: จัดการ CSRF token และการแนบ header ให้อัตโนมัติ
- ✅ ระบบจัดการข้อผิดพลาด: จับและกู้คืนจากข้อผิดพลาดที่พบบ่อย พร้อมวงจร retry
- ✅ สถานะการโหลด: ประสานกับ AuthLoadingManager เพื่อสื่อสารสถานะการทำงานแก่ผู้ใช้
- ✅ บริหารเซสชัน: เก็บและซิงก์สถานะผู้ใช้ร่วมกับ localStorage / SessionStorage
- ✅ โซเชียลล็อกอิน: เชื่อมต่อผู้ให้บริการ OAuth เช่น Google, Facebook ได้ทันที
เมื่อควรเลือกใช้ระบบนี้
✅ แนะนำให้ใช้เมื่อ:
- จำเป็นต้องยืนยันตัวตนผู้ใช้ก่อนเข้าถึงข้อมูลหรือฟีเจอร์สำคัญ
- ต้องการระบบกำหนดสิทธิ์ตามบทบาท (RBAC) หรือสิทธิ์ระดับละเอียด
- ต้องใช้โทเค็นเพื่อทำงานร่วมกับ API ภายนอกหรือบริการไมโครเซอร์วิส
- ต้องการเติมเต็มประสบการณ์ผู้ใช้ เช่น รีเฟรชโทเค็นอัตโนมัติหรือจำเส้นทางที่ตั้งใจเข้าถึง
- มีแผนผนวกบัญชีโซเชียลเข้ากับระบบการยืนยันตัวตน
❌ ควรพิจารณาทางเลือกอื่นหาก:
- แอปพลิเคชันเปิดให้เข้าถึงข้อมูลแบบสาธารณะโดยไม่ต้องล็อกอิน
- โปรเจกต์ต้องการโค้ดที่เรียบง่ายมากและใช้การยืนยันแบบพื้นฐานเท่านั้น
- มีระบบการยืนยันตัวตนจากภายนอกที่บังคับใช้อยู่แล้วและไม่ต้องการบูรณาการเพิ่มเติม
สถาปัตยกรรมระบบ
โซลูชัน Authentication ของ Now.js แบ่งออกเป็น 5 ชั้นประกอบหลัก เพื่อให้ปรับแต่งและขยายได้ง่ายดังแผนภาพต่อไปนี้:
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Router │ │ Views │ │ Components │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Authentication Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ AuthGuard │ │ AuthManager │ │ Error Handler│ │
│ │ (Route Guard)│ │(Core Manager)│ │(Error Handle)│ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ ┌─────────────┼─────────────┐ │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │TokenService │ │LoadingManager│ │ SecurityMgr │ │
│ │(Token Mgmt) │ │(Loading UI) │ │(CSRF, etc.) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Storage Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Cookies │ │ LocalStorage │ │SessionStorage│ │
│ │(HttpOnly JWT)│ │(User Data) │ │(Temp Data) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘ลำดับการไหลของข้อมูลหลัก
-
การเข้าสู่ระบบของผู้ใช้:
User → AuthManager.login() → API → Response (JWT) → TokenService.store() → HttpOnly Cookie → AuthManager.setAuthenticatedUser() → Update State -
การเข้าถึงเส้นทางที่ต้องยืนยันตัวตน:
Router → AuthGuard.checkRoute() → AuthManager.checkAuthStatus() → TokenService.getToken() → Validate Token → Check Permissions/Roles → Allow/Deny -
การรีเฟรชโทเค็น:
Request → 401 Error → AuthManager.refreshToken() → TokenService.getRefreshToken() → API → New Access Token → Continue Request
คอมโพเนนต์หลัก
1. AuthManager (Core)
ไฟล์: Now/js/AuthManager.js
หน้าที่: ดูแลวงจรการยืนยันตัวตนตั้งแต่เริ่มต้นจนจบ
ฟีเจอร์เด่น:
- กระบวนการเข้าสู่ระบบและออกจากระบบที่ครบถ้วน
- บริหาร access token และ refresh token
- จัดการสถานะเซสชันและผู้ใช้
- ต่ออายุโทเค็นอัตโนมัติเมื่อใกล้หมดอายุ
- เชื่อมต่อโซเชียลล็อกอินและ OAuth ได้ทันที
- ป้องกันการโจมตีด้วย CSRF ในตัว
เอกสาร: AuthManager.md
2. TokenService
ไฟล์: Now/js/TokenService.js
หน้าที่: จัดการโทเค็น JWT ตั้งแต่การแยกวิเคราะห์ การตรวจสอบ ไปจนถึงการจัดเก็บ
ฟีเจอร์เด่น:
- แยกวิเคราะห์ payload ของ JWT ได้สะดวก
- ตรวจสอบอายุและความถูกต้องของโทเค็น
- จัดเก็บโทเค็นในคุกกี้หรือ localStorage ตามนโยบายที่ตั้งไว้
- ดึงข้อมูลผู้ใช้จากโทเค็นเพื่อใช้งานต่อ
เอกสาร: TokenService.md
3. AuthGuard
ไฟล์: Now/js/AuthGuard.js
หน้าที่: ป้องกันเส้นทาง (route) ตามเงื่อนไขการยืนยันตัวตนและสิทธิ์การเข้าถึง
ฟีเจอร์เด่น:
- กำหนดเส้นทางที่ต้องล็อกอินก่อนเข้าถึง
- ตรวจสอบบทบาทผู้ใช้ (role-based access control)
- ตรวจสอบสิทธิ์เชิงละเอียด (permission checking)
- รองรับการเพิ่มกฎตรวจสอบพิเศษเพิ่มเติม
- จดจำ URL ที่ผู้ใช้ตั้งใจเข้าก่อนถูกเปลี่ยนเส้นทาง
เอกสาร: AuthGuard.md
4. AuthErrorHandler
ไฟล์: Now/js/AuthErrorHandler.js
หน้าที่: จัดการข้อผิดพลาดที่เกี่ยวข้องกับการยืนยันตัวตนและวางมาตรการตอบสนอง
ฟีเจอร์เด่น:
- รองรับการจัดการข้อผิดพลาดมากกว่า 11 ประเภท
- ดำเนินการตอบสนองอัตโนมัติ เช่น redirect, render, retry
- รีเฟรชโทเค็นเมื่อหมดอายุหากเป็นไปได้
- ปรับใช้มาตรการจำกัดความถี่ (rate limiting)
- เปิดให้กำหนดการจัดการข้อผิดพลาดเพิ่มเติมได้เอง
เอกสาร: AuthErrorHandler.md
5. AuthLoadingManager
ไฟล์: Now/js/AuthLoadingManager.js
หน้าที่: ควบคุมสถานะการโหลดที่เกี่ยวข้องกับกระบวนการยืนยันตัวตนทั้งหมด
ฟีเจอร์เด่น:
- ครอบคลุมสถานะการโหลดมากกว่า 9 ประเภทของการทำงาน
- แสดง overlay/loading UI ตามมาตรฐานของระบบ
- ติดตามความคืบหน้าการทำงานที่ใช้เวลานาน
- จัดการกรณีหมดเวลา (timeout) ได้อัตโนมัติ
- ปรับแต่ง UI การโหลดเพิ่มเติมได้ตามความต้องการ
เอกสาร: AuthLoadingManager.md
เริ่มต้นใช้งานอย่างรวดเร็ว
1. การติดตั้ง
ระบบยืนยันตัวตนถูกบันเดิลมาพร้อม Now.js Framework อยู่แล้ว:
<!-- โหลด AuthManager และโมดูลที่เกี่ยวข้อง -->
<script src="/Now/js/TokenService.js"></script>
<script src="/Now/js/AuthLoadingManager.js"></script>
<script src="/Now/js/AuthErrorHandler.js"></script>
<script src="/Now/js/AuthGuard.js"></script>
<script src="/Now/js/AuthManager.js"></script>2. การตั้งค่าพื้นฐาน
// เริ่มต้นใช้งาน AuthManager
await AuthManager.init({
enabled: true,
type: 'jwt-httponly',
endpoints: {
login: '/api/v1/auth/login',
logout: '/api/v1/auth/logout',
verify: '/api/v1/auth/verify',
refresh: '/api/v1/auth/refresh'
},
security: {
csrf: true,
autoRefresh: true
},
redirects: {
afterLogin: '/dashboard',
afterLogout: '/login',
unauthorized: '/login'
}
});
console.log('ระบบยืนยันตัวตนพร้อมใช้งานแล้ว!');3. การเข้าสู่ระบบของผู้ใช้
// ตัวอย่างการเข้าสู่ระบบแบบพื้นฐาน
const result = await AuthManager.login({
username: 'user@example.com',
password: 'secret123'
});
if (result.success) {
console.log('เข้าสู่ระบบสำเร็จ!');
console.log('ข้อมูลผู้ใช้:', AuthManager.getUser());
// ระบบจะพาไปยัง /dashboard ให้อัตโนมัติ
}4. ตรวจสอบสถานะการยืนยันตัวตน
// ตรวจสอบว่าผู้ใช้ล็อกอินแล้วหรือไม่
if (AuthManager.isAuthenticated()) {
const user = AuthManager.getUser();
console.log('ยินดีต้อนรับ', user.name);
} else {
console.log('โปรดเข้าสู่ระบบ');
}5. ออกจากระบบ
// ออกจากระบบผู้ใช้ปัจจุบัน
await AuthManager.logout();
console.log('ออกจากระบบเรียบร้อย');
// ระบบจะพาไปหน้า /login ให้อัตโนมัติลำดับขั้นการยืนยันตัวตน
1. ลำดับการเข้าสู่ระบบ (ครบวงจร)
// ขั้นที่ 1: ผู้ใช้ส่งฟอร์มเข้าสู่ระบบ
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
// ขั้นที่ 2: เรียก AuthManager.login()
const result = await AuthManager.login({
username: document.getElementById('username').value,
password: document.getElementById('password').value
});
// ขั้นที่ 3: ตรวจสอบผลลัพธ์
if (result.success) {
// เข้าสู่ระบบสำเร็จ
// - JWT ถูกเก็บในคุกกี้ HttpOnly
// - ข้อมูลผู้ใช้ถูกเก็บใน localStorage
// - ระบบจะนำไปหน้า dashboard ให้อัตโนมัติ
console.log('เข้าสู่ระบบสำเร็จ!');
} else {
// เข้าสู่ระบบไม่สำเร็จ
console.error('เข้าสู่ระบบล้มเหลว:', result.error);
alert(result.error);
}
});กระบวนการเบื้องหลัง:
AuthLoadingManagerแสดง loading overlayAuthManagerเรียก login endpoint- Server ตรวจสอบ credentials
- Server ส่ง JWT tokens กลับมา
TokenServiceเก็บ access token ใน httpOnly cookieTokenServiceเก็บ refresh token ใน httpOnly cookie (ถ้ามี)AuthManagerเก็บ user data ใน localStorageAuthManagerอัปเดต state (authenticated = true)AuthManagerredirect ไปafterLoginrouteAuthLoadingManagerปิด loading overlay
2. ลำดับการตรวจเส้นทางที่ต้องยืนยันตัวตน
// ตั้งค่า Router พร้อม auth guard
await Router.init({
routes: [
{
path: '/dashboard',
handler: 'renderDashboard',
metadata: {
requiresAuth: true,
roles: ['user', 'admin'],
permissions: ['read:dashboard']
}
}
],
auth: {
enabled: true,
guard: AuthGuard.checkRoute,
defaultRequireAuth: false,
redirects: {
unauthorized: '/login',
forbidden: '/403'
}
}
});
// เมื่อผู้ใช้ไปยัง /dashboard
Router.navigate('/dashboard');กระบวนการเบื้องหลัง:
- Router เรียก
AuthGuard.checkRoute() AuthGuardขอให้AuthManager.checkAuthStatus()ตรวจสอบสถานะAuthManagerตรวจสอบการยืนยันตัวตนปัจจุบัน- หากมีโทเค็นแต่ยังไม่ยืนยัน → เรียก verify endpoint
AuthGuardตรวจสอบบทบาทและสิทธิ์ที่ตั้งไว้- หากผ่านเงื่อนไข → อนุญาตให้เข้าหน้าเป้าหมาย
- หากไม่ผ่าน → เปลี่ยนเส้นทางไปหน้า unauthorized
3. ลำดับการรีเฟรชโทเค็น
// การรีเฟรชโทเค็นอัตโนมัติ (ตั้งค่าไว้ใน AuthManager)
await AuthManager.init({
security: {
autoRefresh: true,
refreshBeforeExpiry: 5 * 60 * 1000 // 5 minutes
}
});
// การรีเฟรชโทเค็นด้วยตนเอง
const refreshed = await AuthManager.refreshToken();
if (refreshed) {
console.log('รีเฟรชโทเค็นสำเร็จ');
}กระบวนการเบื้องหลัง:
AuthManagerตั้ง timer ก่อน token หมดอายุ 5 นาที- Timer trigger → call
refreshToken() TokenServiceอ่าน refresh token จาก cookieAuthManagerเรียก refresh endpoint พร้อม refresh token- Server validate refresh token
- Server ส่ง access token ใหม่กลับมา
TokenServiceเก็บ access token ใหม่AuthManagerตั้ง timer ใหม่
ตัวอย่างรีเฟรชอัตโนมัติเมื่อเจอ 401:
// กำหนดใน interceptor ของ HttpClient
httpClient.on('response', async (response) => {
if (response.status === 401) {
// พยายามรีเฟรชโทเค็นใหม่
const refreshed = await AuthManager.refreshToken();
if (refreshed) {
// ส่งคำร้องเดิมอีกครั้งด้วยโทเค็นใหม่
return httpClient.retry(response.config);
} else {
// รีเฟรชไม่สำเร็จ - ออกจากระบบผู้ใช้
await AuthManager.logout(false);
Router.navigate('/login');
}
}
});4. ลำดับการจัดการข้อผิดพลาด
// AuthErrorHandler ถูกเชื่อมต่อให้อัตโนมัติ
// แต่สามารถปรับแต่งการจัดการข้อผิดพลาดเพิ่มเติมได้
document.addEventListener('auth:error', (e) => {
const { errorType, message, action } = e.detail;
console.log('ข้อผิดพลาดด้านการยืนยันตัวตน:', errorType);
console.log('ข้อความ:', message);
console.log('การดำเนินการที่เกิดขึ้น:', action);
// จัดการเพิ่มเติมตามต้องการ
if (errorType === 'TOKEN_EXPIRED') {
console.log('โทเค็นหมดอายุ กำลังรีเฟรช...');
}
});ประเภทข้อผิดพลาดที่รองรับ:
UNAUTHORIZED- ไม่มีโทเค็นหรือโทเค็นไม่ถูกต้องFORBIDDEN- ไม่มีสิทธิ์เข้าถึงทรัพยากรTOKEN_EXPIRED- โทเค็นหมดอายุTOKEN_INVALID- โทเค็นไม่ถูกต้องSESSION_EXPIRED- เซสชันหมดอายุRATE_LIMITED- ทำคำร้องมากเกินไปACCOUNT_LOCKED- บัญชีถูกล็อกชั่วคราว- และอื่นๆ (รายละเอียดเพิ่มเติมใน AuthErrorHandler.md)
ตัวอย่างการใช้งาน
1. ฟอร์มเข้าสู่ระบบแบบครบวงจร
<!DOCTYPE html>
<html>
<head>
<title>เข้าสู่ระบบ</title>
</head>
<body>
<form id="loginForm">
<input type="text" id="username" placeholder="ชื่อผู้ใช้" required>
<input type="password" id="password" placeholder="รหัสผ่าน" required>
<button type="submit">เข้าสู่ระบบ</button>
<div id="error" style="color: red;"></div>
</form>
<script src="/Now/js/TokenService.js"></script>
<script src="/Now/js/AuthLoadingManager.js"></script>
<script src="/Now/js/AuthErrorHandler.js"></script>
<script src="/Now/js/AuthManager.js"></script>
<script>
// เริ่มต้นใช้ AuthManager
(async () => {
await AuthManager.init({
enabled: true,
endpoints: {
login: '/api/v1/auth/login',
logout: '/api/v1/auth/logout'
},
redirects: {
afterLogin: '/dashboard'
}
});
// จัดการการส่งฟอร์มเข้าสู่ระบบ
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const errorDiv = document.getElementById('error');
errorDiv.textContent = '';
try {
const result = await AuthManager.login({
username: document.getElementById('username').value,
password: document.getElementById('password').value
});
if (result.success) {
// เข้าสู่ระบบสำเร็จ - ระบบจะเปลี่ยนหน้าให้เอง
console.log('เข้าสู่ระบบสำเร็จ!');
} else {
// แสดงข้อความผิดพลาด
errorDiv.textContent = result.error || 'เข้าสู่ระบบไม่สำเร็จ';
}
} catch (error) {
errorDiv.textContent = 'เกิดข้อผิดพลาด โปรดลองอีกครั้ง';
console.error('ข้อผิดพลาดในการเข้าสู่ระบบ:', error);
}
});
})();
</script>
</body>
</html>2. แดชบอร์ดที่ต้องยืนยันตัวตนก่อนเข้าถึง
<!DOCTYPE html>
<html>
<head>
<title>แดชบอร์ด</title>
</head>
<body>
<div id="dashboard">
<h1>ยินดีต้อนรับ <span id="userName"></span>!</h1>
<button id="logoutBtn">ออกจากระบบ</button>
</div>
<script src="/Now/js/TokenService.js"></script>
<script src="/Now/js/AuthManager.js"></script>
<script>
(async () => {
// เริ่มต้นใช้งาน AuthManager
await AuthManager.init({
enabled: true
});
// ตรวจสอบสถานะการยืนยันตัวตน
if (!AuthManager.isAuthenticated()) {
// ยังไม่เข้าสู่ระบบ - พาไปหน้าเข้าสู่ระบบ
window.location.href = '/login';
return;
}
// ดึงข้อมูลผู้ใช้
const user = AuthManager.getUser();
document.getElementById('userName').textContent = user.name;
// จัดการการออกจากระบบ
document.getElementById('logoutBtn').addEventListener('click', async () => {
await AuthManager.logout();
// ระบบจะเปลี่ยนหน้าไป /login ให้อัตโนมัติ
});
})();
</script>
</body>
</html>3. ส่วนติดต่อผู้ใช้ตามบทบาท
// แสดงหรือซ่อนองค์ประกอบตามบทบาทของผู้ใช้
(async () => {
await AuthManager.init({ enabled: true });
if (!AuthManager.isAuthenticated()) {
window.location.href = '/login';
return;
}
const user = AuthManager.getUser();
// ตรวจสอบบทบาท
if (AuthManager.hasRole('admin')) {
// แสดงแดชบอร์ดผู้ดูแลระบบ
document.getElementById('adminPanel').style.display = 'block';
}
// ตรวจสอบสิทธิ์
if (AuthManager.hasPermission('edit:posts')) {
// แสดงปุ่มแก้ไข
document.getElementById('editButton').style.display = 'block';
}
})();4. เรียกใช้งาน API พร้อมการยืนยันตัวตน
// ApiService จะส่งโทเค็นยืนยันตัวตนให้โดยอัตโนมัติ
const response = await ApiService.get('/api/users');
if (response.ok) {
console.log('ข้อมูลผู้ใช้:', response.data);
} else if (response.status === 401) {
// โทเค็นหมดอายุ - AuthManager จะจัดการให้เอง
console.log('โทเค็นหมดอายุ กำลังรีเฟรช...');
} else {
console.error('ข้อผิดพลาด:', response.error);
}5. การเข้าสู่ระบบผ่านโซเชียล
// เริ่มต้นพร้อมกำหนด endpoint สำหรับโซเชียลล็อกอิน
await AuthManager.init({
enabled: true,
endpoints: {
social: '/api/v1/auth/social/{provider}',
callback: '/api/v1/auth/callback'
}
});
// ปุ่มเข้าสู่ระบบด้วย Google
document.getElementById('googleLogin').addEventListener('click', async () => {
await AuthManager.socialLogin('google', {
redirect_uri: 'http://localhost/auth/callback'
});
// ระบบจะเปิดหน้าต่างป็อปอัปสำหรับ Google OAuth
});
// ปุ่มเข้าสู่ระบบด้วย Facebook
document.getElementById('facebookLogin').addEventListener('click', async () => {
await AuthManager.socialLogin('facebook', {
redirect_uri: 'http://localhost/auth/callback'
});
});
// จัดการผลลัพธ์หลังกลับจากผู้ให้บริการโซเชียล
if (window.location.pathname === '/auth/callback') {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const provider = params.get('provider');
if (code && provider) {
const result = await AuthManager.handleAuthCallback({
code,
provider
});
if (result.success) {
console.log('เข้าสู่ระบบผ่านโซเชียลสำเร็จ!');
window.location.href = '/dashboard';
}
}
}6. การตรวจสอบเพิ่มเติมตามเงื่อนไขเฉพาะ
// ตั้งค่า Router พร้อมกฎตรวจสอบเพิ่มเติม
await Router.init({
routes: [
{
path: '/admin/settings',
handler: 'renderSettings',
metadata: {
requiresAuth: true,
validate: async (to, from, router) => {
const user = AuthManager.getUser();
// ตรวจสอบสถานะการสมัครใช้งาน (subscription)
if (!user.subscription || user.subscription.status !== 'active') {
return {
allowed: false,
action: 'redirect',
target: '/subscribe',
reason: 'ต้องมีการสมัครใช้งานที่ใช้งานอยู่'
};
}
// ตรวจสอบว่า IP อยู่ใน whitelist
const response = await fetch('/api/check-ip');
const { allowed } = await response.json();
if (!allowed) {
return {
allowed: false,
action: 'block',
reason: 'IP not whitelisted'
};
}
return { allowed: true };
}
}
}
],
auth: {
enabled: true,
guard: AuthGuard.checkRoute
}
});แนวทางปฏิบัติที่แนะนำ
1. ความปลอดภัยของโทเค็น
✅ ควรทำ:
- ใช้คุกกี้แบบ httpOnly สำหรับเก็บโทเค็น
- ตั้งค่า
secureสำหรับสภาพแวดล้อมจริง (ต้องใช้ HTTPS) - ใช้ค่า SameSite =
StrictหรือLaxตามความเหมาะสม - หมุนเวียน (rotate) refresh token เป็นประจำ
- กำหนดอายุโทเค็นให้เหมาะสม (เช่น access: 15 นาที, refresh: 7 วัน)
❌ ไม่ควรทำ:
- เก็บโทเค็นใน localStorage เพราะเสี่ยงต่อ XSS
- เก็บข้อมูลอ่อนไหวไว้ใน payload ของ JWT
- ส่งโทเค็นผ่าน URL parameters
- ใช้โทเค็นที่ไม่มีวันหมดอายุ
// ✅ ตัวอย่างที่ดี - เก็บโทเค็นในคุกกี้ HttpOnly
await AuthManager.init({
type: 'jwt-httponly',
token: {
cookieOptions: {
httpOnly: true, // ป้องกันการเข้าถึงผ่าน JavaScript
secure: true, // ใช้ได้เฉพาะผ่าน HTTPS
sameSite: 'Strict'
}
}
});
// ❌ ตัวอย่างที่ไม่ควรทำ - เก็บใน localStorage
localStorage.setItem('token', token); // เสี่ยงต่อการถูกโจมตีแบบ XSS2. การป้องกัน CSRF
✅ ควรทำ:
- เปิดใช้งานระบบป้องกัน CSRF
- ส่งค่า CSRF token ผ่าน header ทุกครั้งที่จำเป็น
- ตรวจสอบความถูกต้องของ CSRF token บนเซิร์ฟเวอร์
- ใช้คุกกี้ที่ตั้งค่า SameSite เพื่อลดการปลอมคำร้อง
// ✅ ตัวอย่างที่ดี - เปิดระบบป้องกัน CSRF
await AuthManager.init({
security: {
csrf: true,
csrfIncludeSafeMethods: true
}
});
// CSRF token จะถูกแนบในคำร้องให้อัตโนมัติ
const response = await ApiService.post('/api/users', userData);3. การจัดการข้อผิดพลาด
✅ ควรทำ:
- รองรับการจัดการข้อผิดพลาดทุกประเภทที่ระบบอาจส่งมา
- แสดงข้อความที่ผู้ใช้เข้าใจได้ง่าย
- บันทึกข้อผิดพลาดเพื่อช่วยดีบัก
- ออกแบบการลองทำซ้ำ (retry) ให้เหมาะกับแต่ละกรณี
// ✅ ตัวอย่างที่ดี - จัดการข้อผิดพลาดอย่างครอบคลุม
document.addEventListener('auth:error', (e) => {
const { errorType, message } = e.detail;
// บันทึกข้อความสำหรับการดีบัก
console.error('ข้อผิดพลาดของระบบยืนยันตัวตน:', errorType, message);
// แสดงข้อความให้ผู้ใช้เข้าใจง่าย
switch (errorType) {
case 'TOKEN_EXPIRED':
showNotification('เซสชันหมดอายุแล้ว โปรดเข้าสู่ระบบใหม่');
break;
case 'FORBIDDEN':
showNotification('คุณไม่มีสิทธิ์เข้าถึงหน้านี้');
break;
default:
showNotification('เกิดข้อผิดพลาด โปรดลองอีกครั้ง');
}
});4. การจัดการสถานะการโหลด
✅ ควรทำ:
- แสดงตัวบ่งชี้สถานะโหลดสำหรับงานแบบอะซิงโครนัส
- ให้ข้อความตอบกลับที่ชัดเจนกับผู้ใช้
- ปิดการใช้งานปุ่มระหว่างกำลังประมวลผล
- กำหนดเวลาหมดเขต (timeout) ที่เหมาะสม
// ✅ ตัวอย่างที่ดี - จัดการสถานะโหลดอย่างเหมาะสม
async function handleLogin(credentials) {
const submitBtn = document.getElementById('submitBtn');
// ปิดการใช้งานปุ่มระหว่างล็อกอิน
submitBtn.disabled = true;
submitBtn.textContent = 'กำลังเข้าสู่ระบบ...';
try {
const result = await AuthManager.login(credentials);
if (result.success) {
// แจ้งผลลัพธ์ที่สำเร็จ
submitBtn.textContent = 'สำเร็จ! กำลังเปลี่ยนหน้า...';
}
} catch (error) {
// แจ้งเมื่อเกิดข้อผิดพลาด
submitBtn.textContent = 'เข้าสู่ระบบไม่สำเร็จ';
} finally {
// เปิดให้ใช้ปุ่มอีกครั้ง
setTimeout(() => {
submitBtn.disabled = false;
submitBtn.textContent = 'เข้าสู่ระบบ';
}, 2000);
}
}5. การจัดการเซสชัน
✅ ควรทำ:
- ตรวจสอบสถานะเซสชันทุกครั้งเมื่อผู้ใช้กลับมาที่หน้าเว็บ
- ล้างข้อมูลเซสชันทุกครั้งที่ผู้ใช้ออกจากระบบ
- รับมือกรณีเซสชันหมดอายุอย่างเหมาะสม
- กำหนดเวลาหมดอายุของเซสชันให้สัมพันธ์กับความเสี่ยงของระบบ
// ✅ ตัวอย่างที่ดี - ตรวจสอบเซสชันเมื่อโหลดหน้า
window.addEventListener('load', async () => {
await AuthManager.init({ enabled: true });
if (AuthManager.isAuthenticated()) {
// ตรวจสอบว่าเซสชันยังคงใช้ได้อยู่หรือไม่
const valid = await AuthManager.checkAuthStatus();
if (!valid) {
// เซสชันหมดอายุ
console.log('เซสชันหมดอายุ โปรดเข้าสู่ระบบใหม่');
await AuthManager.logout(false); // ไม่ต้องเรียกเซิร์ฟเวอร์
window.location.href = '/login';
}
}
});
// ตรวจสอบเมื่อผู้ใช้กลับมาที่หน้าอีกครั้ง
document.addEventListener('visibilitychange', async () => {
if (!document.hidden && AuthManager.isAuthenticated()) {
// ผู้ใช้กลับมาที่หน้า - ตรวจสอบเซสชันอีกครั้ง
await AuthManager.checkAuthStatus();
}
});6. การป้องกันเส้นทาง
✅ ควรทำ:
- ใช้ AuthGuard สำหรับเส้นทางที่ต้องการการยืนยันตัวตน
- กำหนดค่าเริ่มต้นว่าหน้าใดจำเป็นต้องล็อกอิน
- ระบุบทบาทและสิทธิ์ให้ชัดเจนสำหรับแต่ละเส้นทาง
- ออกแบบประสบการณ์เมื่อไม่มีสิทธิ์เข้าถึงให้เหมาะสม
// ✅ ตัวอย่างที่ดี - ป้องกันเส้นทางอย่างรอบด้าน
await Router.init({
routes: [
// เส้นทางสำหรับบุคคลทั่วไป
{ path: '/', handler: 'home', metadata: { public: true } },
{ path: '/about', handler: 'about', metadata: { public: true } },
// เส้นทางเฉพาะผู้เยี่ยมชม (ยังไม่ล็อกอิน)
{
path: '/login',
handler: 'login',
metadata: {
guestOnly: true,
redirectOnAuth: '/dashboard'
}
},
// เส้นทางที่ต้องล็อกอินและมีบทบาทเฉพาะ
{
path: '/dashboard',
handler: 'dashboard',
metadata: {
requiresAuth: true,
roles: ['user', 'admin']
}
},
// เส้นทางสำหรับผู้ดูแลระบบเท่านั้น
{
path: '/admin/*',
handler: 'admin',
metadata: {
requiresAuth: true,
roles: ['admin'],
permissions: ['admin:access']
}
}
],
auth: {
enabled: true,
guard: AuthGuard.checkRoute,
defaultRequireAuth: false,
redirects: {
unauthorized: '/login',
forbidden: '/403'
}
}
});7. การทดสอบระบบยืนยันตัวตน
✅ ควรทำ:
- ทดสอบทุกลำดับขั้นการยืนยันตัวตน
- ทดสอบสถานการณ์ที่เกิดข้อผิดพลาด
- ทดสอบกระบวนการรีเฟรชโทเค็น
- ทดสอบการป้องกันเส้นทางตามบทบาทและสิทธิ์
// ✅ ตัวอย่างที่ดี - ครอบคลุมกรณีทดสอบสำคัญ
describe('Authentication', () => {
beforeEach(async () => {
await AuthManager.init({ enabled: true });
AuthManager.clearAuthData();
});
it('should login successfully', async () => {
const result = await AuthManager.login({
username: 'test@example.com',
password: 'password123'
});
expect(result.success).toBe(true);
expect(AuthManager.isAuthenticated()).toBe(true);
});
it('should handle invalid credentials', async () => {
const result = await AuthManager.login({
username: 'test@example.com',
password: 'wrongpassword'
});
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
it('should refresh token before expiry', async () => {
// เข้าสู่ระบบก่อน
await AuthManager.login({ username: 'test', password: 'test' });
// สร้างโทเค็นจำลองที่ใกล้หมดอายุ
const token = TokenService.getToken();
const payload = TokenService.parseToken(token);
payload.exp = Math.floor(Date.now() / 1000) + 240; // 4 minutes
// ควรกระตุ้นให้ระบบรีเฟรชอัตโนมัติ
await new Promise(resolve => setTimeout(resolve, 1000));
expect(AuthManager.state.refreshTimer).toBeTruthy();
});
it('should protect routes based on roles', async () => {
// เข้าสู่ระบบด้วยผู้ใช้ทั่วไป
await AuthManager.login({ username: 'user', password: 'test' });
// พยายามเข้าหน้าผู้ดูแลระบบ
const result = await AuthGuard.checkRoute(
{ path: '/admin', metadata: { roles: ['admin'] } },
null,
Router
);
expect(result.allowed).toBe(false);
expect(result.action).toBe('redirect');
});
});ข้อควรคำนึงด้านความปลอดภัย
1. ความปลอดภัยของโทเค็น
ความเสี่ยงที่ต้องระวัง:
- การโจมตีแบบ XSS (Cross-Site Scripting)
- การโจมตีแบบ CSRF (Cross-Site Request Forgery)
- การขโมยโทเค็น
- การสวมรอยเซสชันของผู้ใช้ (Session Hijacking)
แนวทางป้องกัน:
// ✅ ใช้คุกกี้แบบ httpOnly
await AuthManager.init({
type: 'jwt-httponly',
token: {
cookieOptions: {
httpOnly: true, // ป้องกันการถูกอ่านผ่าน JavaScript (ลดความเสี่ยง XSS)
secure: true, // ใช้ผ่าน HTTPS เท่านั้น
sameSite: 'Strict' // ลดโอกาสถูกโจมตีแบบ CSRF
}
},
security: {
csrf: true // เปิดระบบป้องกัน CSRF
}
});
// ✅ ทำความสะอาดข้อมูลที่กรอกเข้ามา
const sanitize = (str) => {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
const username = sanitize(userInput);2. การจำกัดความถี่ (Rate Limiting)
ความเสี่ยงที่ต้องระวัง:
- การเดารหัสผ่านแบบรัวๆ (Brute Force)
- การโจมตีแบบ DDoS
แนวทางป้องกัน:
// ✅ เปิดใช้งานการจำกัดความถี่
await AuthManager.init({
security: {
rateLimiting: true,
maxLoginAttempts: 5,
lockoutTime: 30 * 60 * 1000 // 30 minutes
}
});
// ตัวอย่างบนฝั่งเซิร์ฟเวอร์
// ใช้ไลบรารีอย่าง express-rate-limit
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests per windowMs
message: 'พยายามเข้าสู่ระบบบ่อยเกินไป โปรดลองอีกครั้งภายหลัง'
});
app.post('/api/v1/auth/login', loginLimiter, (req, res) => {
// โค้ดจัดการการเข้าสู่ระบบ
});3. ความปลอดภัยของรหัสผ่าน
แนวทางที่ควรทำ:
- ไม่เก็บรหัสผ่านเป็นข้อความธรรมดา (plaintext)
- ใช้อัลกอริทึมแฮชที่รัดกุม เช่น bcrypt หรือ argon2
- กำหนดคุณสมบัติขั้นต่ำของรหัสผ่าน
- ให้ผู้ใช้เห็นระดับความแข็งแรงของรหัสผ่าน
// ✅ ตรวจสอบฝั่งไคลเอนต์
function validatePassword(password) {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*]/.test(password);
if (password.length < minLength) {
return { valid: false, message: 'รหัสผ่านต้องมีความยาวอย่างน้อย 8 ตัวอักษร' };
}
if (!hasUpperCase || !hasLowerCase || !hasNumbers) {
return {
valid: false,
message: 'รหัสผ่านต้องมีทั้งตัวพิมพ์ใหญ่ ตัวพิมพ์เล็ก และตัวเลข'
};
}
return { valid: true };
}
// การแฮชรหัสผ่านฝั่งเซิร์ฟเวอร์ (ตัวอย่าง Node.js)
const bcrypt = require('bcrypt');
const saltRounds = 10;
// แฮชรหัสผ่าน
const hashedPassword = await bcrypt.hash(password, saltRounds);
// ตรวจสอบรหัสผ่าน
const match = await bcrypt.compare(password, hashedPassword);4. ความปลอดภัยของเซสชัน
แนวทางที่ควรทำ:
- กำหนดเวลา timeout ของเซสชันให้เหมาะสม
- ล้างข้อมูลเซสชันทุกครั้งที่ออกจากระบบ
- ตรวจสอบความถูกต้องของเซสชันก่อนดำเนินการที่สำคัญ
- ใช้พื้นที่จัดเก็บเซสชันที่ปลอดภัย
// ✅ จัดการเวลา timeout ของเซสชัน
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
let sessionTimer;
function resetSessionTimer() {
clearTimeout(sessionTimer);
sessionTimer = setTimeout(() => {
console.log('เซสชันหมดเวลา - กำลังออกจากระบบ');
AuthManager.logout();
}, SESSION_TIMEOUT);
}
// รีเซ็ตตัวจับเวลาเมื่อผู้ใช้มีการใช้งาน
document.addEventListener('click', resetSessionTimer);
document.addEventListener('keypress', resetSessionTimer);
// ล้างตัวจับเวลาเมื่อออกจากระบบ
AuthManager.on('logout', () => {
clearTimeout(sessionTimer);
});5. ความปลอดภัยของ API
แนวทางที่ควรทำ:
- ใช้ HTTPS สำหรับการสื่อสารทุกครั้ง
- ตรวจสอบความถูกต้องของข้อมูลที่รับเข้ามา
- ทำความสะอาดข้อมูลก่อนส่งออก
- ตั้งค่า CORS อย่างเหมาะสม
// ✅ บังคับใช้งานผ่าน HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}
// ✅ การตั้งค่า CORS (ตัวอย่างฝั่งเซิร์ฟเวอร์)
app.use(cors({
origin: 'https://yourdomain.com',
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token']
}));
// ✅ ตรวจสอบข้อมูลที่ส่งเข้ามา
function validateLoginInput(credentials) {
if (!credentials.username || !credentials.password) {
throw new Error('ต้องกรอกชื่อผู้ใช้และรหัสผ่าน');
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(credentials.username)) {
throw new Error('รูปแบบอีเมลไม่ถูกต้อง');
}
return true;
}เอกสารที่เกี่ยวข้อง
คอมโพเนนต์หลัก
- AuthManager.md - ผู้จัดการการยืนยันตัวตนหลักของระบบ
- TokenService.md - บริหารจัดการโทเค็น JWT
- AuthGuard.md - ป้องกันเส้นทางและตรวจสอบสิทธิ์
- AuthErrorHandler.md - จัดการข้อผิดพลาดด้านการยืนยันตัวตน
- AuthLoadingManager.md - ควบคุมสถานะการโหลดสำหรับงานด้าน auth
ระบบที่เกี่ยวข้อง
- Router.md - ระบบกำหนดเส้นทาง (ใช้งานร่วมกับ AuthGuard)
- ApiService.md - บริการ HTTP ที่แนบโทเค็นให้อัตโนมัติ
- HttpClient.md - คลาส HTTP ระดับล่างสำหรับปรับแต่งคำร้อง
คู่มือ
- Quick Start Guide - คู่มือเริ่มต้นใช้งานอย่างรวดเร็ว
- Social Login Guide - ขั้นตอนการตั้งค่าโซเชียลล็อกอิน
- Security Best Practices - แนวทางรักษาความปลอดภัยเพิ่มเติม
ความเข้ากันได้กับเบราว์เซอร์
ระบบการยืนยันตัวตนรองรับเบราว์เซอร์สมัยใหม่ดังนี้:
- ✅ Chrome 90 ขึ้นไป
- ✅ Firefox 88 ขึ้นไป
- ✅ Safari 14 ขึ้นไป
- ✅ Edge 90 ขึ้นไป
- ❌ IE 11 (ไม่รองรับ)
ฟีเจอร์ที่จำเป็นต้องใช้:
- การรองรับ Promises และ async/await
- Fetch API
- ความสามารถของ JavaScript รุ่น ES6 ขึ้นไป
- คุกกี้แบบ HttpOnly
- Web Storage API
ข้อควรพิจารณาด้านประสิทธิภาพ
1. การตรวจสอบความถูกต้องของโทเค็น
// ❌ ตัวอย่างที่ไม่ดี - ตรวจสอบทุกคำร้อง
if (!TokenService.isTokenExpired(token)) {
// Make request
}
// ✅ ตัวอย่างที่ดี - แคชผลการตรวจสอบไว้
let tokenValid = true;
let lastCheck = Date.now();
function isTokenValid() {
const now = Date.now();
if (now - lastCheck > 60000) { // ตรวจสอบทุก 1 นาที
tokenValid = !TokenService.isTokenExpired(token);
lastCheck = now;
}
return tokenValid;
}2. ลดจำนวนการเรียก API
// ❌ ตัวอย่างที่ไม่ดี - ตรวจสอบทุกครั้งที่เปลี่ยนหน้า
Router.on('beforeNavigate', async () => {
await AuthManager.checkAuthStatus(); // เรียก API ทุกครั้ง
});
// ✅ ตัวอย่างที่ดี - ตรวจสอบเป็นช่วงเวลา
const VERIFY_INTERVAL = 5 * 60 * 1000; // 5 นาที
let lastVerify = Date.now();
Router.on('beforeNavigate', async () => {
const now = Date.now();
if (now - lastVerify > VERIFY_INTERVAL) {
await AuthManager.checkAuthStatus();
lastVerify = now;
}
});3. ปรับปรุงการจัดเก็บข้อมูล
// ✅ เก็บข้อมูลผู้ใช้อย่างจำเป็นเท่านั้น
const minimalUser = {
id: user.id,
name: user.name,
email: user.email,
roles: user.roles,
permissions: user.permissions
};
AuthManager.setAuthenticatedUser(minimalUser);
// ❌ อย่าเก็บอ็อบเจ็กต์ที่มีข้อมูลมากเกินไป
// AuthManager.setAuthenticatedUser(userWithAllData); // ข้อมูลใหญ่เกิน!การแก้ไขปัญหา
1. โทเค็นไม่ถูกส่งไปกับคำร้อง
ปัญหา: ไม่พบ Authorization header ในคำร้องที่ส่งออกไป
วิธีแก้ไข:
// ตรวจสอบว่า AuthManager ถูกเริ่มต้นแล้วหรือยัง
console.log('สถานะการเริ่มต้นของ AuthManager:', AuthManager.isInitialized());
// ตรวจสอบว่ามีโทเค็นหรือไม่
const token = TokenService.getToken();
console.log('มีโทเค็นหรือไม่:', !!token);
// ตรวจสอบตัวดักคำร้องของ HttpClient
console.log('ตัวดักคำร้องของ HttpClient:', httpClient.interceptors);2. เกิดการเปลี่ยนหน้าไม่รู้จบ (Redirect Loop)
ปัญหา: วนเปลี่ยนหน้าไปมาระหว่างหน้าล็อกอินและแดชบอร์ด
วิธีแก้ไข:
// ตรวจสอบการตั้งค่าเส้นทาง
await Router.init({
routes: [
{
path: '/login',
handler: 'login',
metadata: {
public: true, // หรือใช้ guestOnly: true
guestOnly: true // หากล็อกอินแล้วให้พาไปแดชบอร์ด
}
},
{
path: '/dashboard',
handler: 'dashboard',
metadata: {
requiresAuth: true // หากยังไม่ล็อกอินให้พาไปหน้า login
}
}
]
});3. รีเฟรชโทเค็นไม่สำเร็จ
ปัญหา: การรีเฟรชโทเค็นล้มเหลว
วิธีแก้ไข:
// ตรวจสอบว่ามี refresh token หรือไม่
const refreshToken = AuthManager.getRefreshToken();
console.log('Refresh token exists:', !!refreshToken);
// ตรวจสอบค่า endpoint สำหรับรีเฟรช
console.log('Refresh endpoint:', AuthManager.config.endpoints.refresh);
// รับมือเมื่อรีเฟรชล้มเหลว
document.addEventListener('auth:refresh:failed', (e) => {
console.error('Refresh failed:', e.detail);
// ออกจากระบบแล้วพาไปหน้า login
AuthManager.logout(false);
Router.navigate('/login');
});4. ไม่พบ CSRF Token ในคำร้อง
ปัญหา: CSRF token ไม่ถูกแนบไปกับคำร้อง
วิธีแก้ไข:
// ตรวจสอบว่าเปิดใช้งาน CSRF หรือไม่
console.log('CSRF enabled:', AuthManager.config.security.csrf);
// ตรวจสอบค่าของ CSRF token
const csrfToken = AuthManager.getCSRFToken();
console.log('CSRF token:', csrfToken);
// ตรวจสอบ meta tag ในหน้า HTML
const metaTag = document.querySelector('meta[name="csrf-token"]');
console.log('Meta tag exists:', !!metaTag);
console.log('Meta tag content:', metaTag?.content);แหล่งข้อมูลเพิ่มเติม
ตัวอย่างโค้ด
- Complete Login Example - ตัวอย่างฟอร์มเข้าสู่ระบบแบบครบชุด
- Protected Routes Example - การป้องกันเส้นทางด้วย AuthGuard
- Social Login Example - การผนวกโซเชียลล็อกอิน
- Role-Based UI Example - การแสดง/ซ่อน UI ตามบทบาทผู้ใช้
เอกสาร API
- Server API Specification - สเปกของ endpoint ด้านการยืนยันตัวตน
- JWT Structure - โครงสร้าง payload ของ JWT
- Error Codes - รายการรหัสข้อผิดพลาดและคำอธิบาย