RBAC ในทางปฏิบัติ: การใช้งานการอนุญาต (Authorization) ที่ปลอดภัยสำหรับแอปของคุณ
คุณกำลังประสบปัญหาในการสร้างระบบการอนุญาต (Authorization) ที่ปลอดภัยและขยายได้สำหรับแอปของคุณหรือไม่? การควบคุมการเข้าถึงตามบทบาท (Role-Based Access Control; RBAC) คือมาตรฐานอุตสาหกรรมสำหรับการจัดการสิทธิ์ของผู้ใช้ แต่การนำไปใช้อย่างถูกต้องอาจเป็นเรื่องท้าทาย บทแนะนำนี้จะแสดงวิธีสร้างระบบ RBAC ที่แข็งแกร่งโดยใช้ตัวอย่างจริงของระบบจัดการเนื้อหา (Content Management System; CMS)
เมื่อทำตามคู่มือนี้ คุณจะได้เรียนรู้:
- ✨ วิธีออกแบบและใช้งานสิทธิ์แบบละเอียด (fine-grained) เพื่อควบคุมได้อย่างแม่นยำ
- 🔒 แนวปฏิบัติที่ดีที่สุดในการจัดกลุ่มสิทธิ์เป็นบทบาทที่มีความหมาย
- 👤 เทคนิคการจัดการความเป็นเจ้าของทรัพยากรอย่างมีประสิทธิภาพ
- 🚀 วิธีทำให้ระบบการอนุญาต (Authorization) ของคุณขยายและดูแลรักษาได้ง่าย
- 💡 ตัวอย่างการใช้งานจริงด้วย CMS
ซอร์สโค้ดทั้งหมดของบทแนะนำนี้มีให้ที่ GitHub
ทำความเข้าใจพื้นฐานของ RBAC
การควบคุมการเข้าถึงตามบทบาท (RBAC) ไม่ใช่แค่การกำหนดสิทธิ์ให้กับผู้ใช้เท่านั้น แต่คือการสร้างแนวทางที่มีโครงสร้างสำหรับการอนุญาต (Authorization) ที่สมดุลระหว่างความปลอดภัยกับการดูแลรักษา
คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ RBAC คืออะไร ได้ใน Auth Wiki
นี่คือหลักการสำคัญที่เราจะใช้ในการนำไปใช้งาน:
การออกแบบสิทธิ์แบบละเอียด (Fine-grained permission design)
สิทธิ์แบบละเอียดช่วยให้คุณควบคุมได้อย่างแม่นยำว่าผู้ใช้สามารถทำอะไรในระบบของคุณ แทนที่จะใช้ระดับการเข้าถึงกว้าง ๆ เช่น "admin" หรือ "user" เราจะกำหนดการกระทำเฉพาะที่ผู้ใช้สามารถทำกับทรัพยากร ตัวอย่างเช่น:
read:articles- ดูบทความใด ๆ ในระบบcreate:articles- สร้างบทความใหม่update:articles- แก้ไขบทความที่มีอยู่publish:articles- เปลี่ยนสถานะการเผยแพร่ของบทความ
ความเป็นเจ้าของทรัพยากรและการควบคุมการเข้าถึง
ความเป็นเจ้าของทรัพยากรเป็นแนวคิดพื้นฐานในการออกแบบการอนุญาต (Authorization) ของ CMS ของเรา ในขณะที่ RBAC กำหนดว่าบทบาทต่าง ๆ สามารถทำอะไรได้บ้าง ความเป็นเจ้าของจะเพิ่มมิติส่วนบุคคลให้กับการควบคุมการเข้าถึง:
- ผู้เขียนจะมีสิทธิ์เข้าถึงบทความที่ตนเองสร้างโดยอัตโนมัติ
- โมเดลความเป็นเจ้าของนี้ทำให้ผู้เขียนสามารถดูและแก้ไขเนื้อหาของตนเองได้เสมอ
- ระบบจะตรวจสอบทั้งสิทธิ์ตามบทบาท หรือ ความเป็นเจ้าของเมื่อจัดการบทความ
- ตัวอย่างเช่น แม้ไม่มีสิทธิ์
update:articlesผู้เขียนก็ยังสามารถแก้ไขบทความของตนเองได้ - การออกแบบนี้ช่วยลดความจำเป็นในการเพิ่มสิทธิ์บทบาทพิเศษโดยยังคงความปลอดภัย
แนวทางสองชั้นนี้ (บทบาท + ความเป็นเจ้าของ) ทำให้ระบบใช้งานง่ายและปลอดภัยยิ่งขึ้น Publisher และ Admin ยังสามารถจัดการเนื้อหาทั้งหมดผ่านสิทธิ์บทบาทของตน ในขณะที่ผู้เขียนควบคุมงานของตนเองได้
ออกแบบ API ที่ปลอดภัย
มาเริ่มออกแบบฟังก์ชันหลักของ CMS ผ่าน API endpoint กัน:
GET /api/articles # แสดงรายการบทความทั้งหมด
GET /api/articles/:id # ดูบทความเฉพาะ
POST /api/articles # สร้างบทความใหม่
PATCH /api/articles/:id # แก้ไขบทความ
DELETE /api/articles/:id # ลบบทความ
PATCH /api/articles/:id/published # เปลี่ยนสถานะการเผยแพร่
ใช้งานการควบคุมการเข้าถึงสำหรับ API ของคุณ
สำหรับแต่ละ endpoint เราต้องพิจารณาสองประเด็นของการควบคุมการเข้าถึง:
- ความเป็นเจ้าของทรัพยากร - ผู้ใช้เป็นเจ้าของทรัพยากรนี้หรือไม่?
- สิทธิ์ตามบทบาท - บทบาทของผู้ใช้อนุญาตให้ดำเนินการนี้หรือไม่?
นี่คือวิธีที่เราจะจัดการการเข้าถึงแต่ละ endpoint:
| Endpoint | หลักการควบคุมการเข้าถึง |
|---|---|
| GET /api/articles | - ใครก็ตามที่มีสิทธิ์ list:articles หรือผู้เขียนสามารถดูบทความของตนเองได้ |
| GET /api/articles/:id | - ใครก็ตามที่มีสิทธิ์ read:articles หรือเจ้าของบทความ |
| POST /api/articles | - ใครก็ตามที่มีสิทธิ์ create:articles |
| PATCH /api/articles/:id | - ใครก็ตามที่มีสิทธิ์ update:articles หรือเจ้าของบทความ |
| DELETE /api/articles/:id | - ใครก็ตามที่มีสิทธิ์ delete:articles หรือเจ้าของบทความ |
| PATCH /api/articles/:id/published | - เฉพาะผู้ใช้ที่มีสิทธิ์ publish:articles เท่านั้น |
สร้างระบบสิทธิ์ที่ขยายได้
จากข้อกำหนดการเข้าถึง API ของเรา เราสามารถกำหนดสิทธิ์เหล่านี้ได้:
| สิทธิ์ | คำอธิบาย |
|---|---|
| list:articles | ดูรายการบทความทั้งหมดในระบบ |
| read:articles | อ่านเนื้อหาบทความใด ๆ |
| create:articles | สร้างบทความใหม่ |
| update:articles | แก้ไขบทความใด ๆ |
| delete:articles | ลบบทความใด ๆ |
| publish:articles | เปลี่ยนสถานะการเผยแพร่ |
โปรดสังเกตว่าสิทธิ์เหล่านี้จำเป็นเฉพาะเมื่อเข้าถึงทรัพยากรที่คุณไม่ได้เป็นเจ้าของ เจ้าของบทความสามารถ:
- ดูบทความของตนเอง (ไม่ต้องใช้
read:articles) - แก้ไขบทความของตนเอง (ไม่ต้องใช้
update:articles) - ลบบทความของตนเอง (ไม่ต้องใช้
delete:articles)
สร้างบทบาทที่มีประสิทธิภาพ
เมื่อเรากำหนด API และสิทธิ์แล้ว เราสามารถสร้างบทบาทที่จัดกลุ่มสิทธิ์เหล่านี้อย่างมีเหตุผล:
| สิทธิ์/บทบาท | 👑 Admin | 📝 Publisher | ✍️ Author |
|---|---|---|---|
| คำอธิบาย | เข้าถึงระบบทั้งหมดเพื่อจัดการเนื้อหา | ดูบทความทั้งหมดและควบคุมสถานะการเผยแพร่ | สร้างบทความใหม่ในระบบ |
| list:articles | ✅ | ✅ | ❌ |
| read:articles | ✅ | ✅ | ❌ |
| create:articles | ✅ | ❌ | ✅ |
| update:articles | ✅ | ❌ | ❌ |
| delete:articles | ✅ | ❌ | ❌ |
| publish:articles | ✅ | ✅ | ❌ |
หมายเหตุ: ผู้เขียนจะได้รับสิทธิ์อ่าน/แก้ไข/ลบบทความของตนเองโดยอัตโนมัติ ไม่ขึ้นกับสิทธิ์บทบาท
แต่ละบทบาทถูกออกแบบโดยคำนึงถึงความรับผิดชอบเฉพาะ:
- Admin: ควบคุม CMS ได้ทั้งหมด รวมถึงการจัดการบทความทุกอย่าง
- Publisher: เน้นการตรวจสอบเนื้อหาและจัดการการเผยแพร่
- Author: เชี่ยวชาญด้านการสร้างเนื้อหา
โครงสร้างบทบาทนี้ช่วยแยกความรับผิดชัดเจน:
- Author เน้นสร้างเนื้อหา
- Publisher จัดการคุณภาพและการมองเห็นของเนื้อหา
- Admin ดูแลควบคุมระบบโดยรวม
ตั้งค่า RBAC ใน Logto
ก่อนเริ่ม คุณต้องสร้างบัญชีใน Logto Cloud หรือจะใช้ Logto แบบ self-hosted ด้วย Logto OSS version ก็ได้
แต่สำหรับบทแนะนำนี้ เราจะใช้ Logto Cloud เพื่อความสะดวก
ตั้งค่าแอปพลิเคชันของคุณ
- ไปที่ "Applications" ใน Logto Console เพื่อสร้างแอป React ใหม่
- ชื่อแอปพลิเคชัน: Content Management System
- ประเภทแอปพลิเคชัน: Traditional Web Application
- Redirect URIs: http://localhost:5173/callback

กำหนดทรัพยากร API และสิทธิ์
- ไปที่ "API Resources" ใน Logto Console เพื่อสร้าง API resource ใหม่
- ชื่อ API: CMS API
- API identifier: https://api.cms.com
- เพิ่มสิทธิ์ให้กับ API resource
list:articlesread:articlescreate:articlesupdate:articlespublish:articlesdelete:articles

สร้างบทบาท
ไปที่ Roles ใน Logto Console เพื่อสร้างบทบาทต่อไปนี้สำหรับ CMS
- Admin
- มีสิทธิ์ทั้งหมด
- Publisher
- มี
read:articles,list:articles,publish:articles
- มี
- Author
- มี
create:articles
- มี



กำหนดบทบาทให้ผู้ใช้
ไปที่ส่วน "User management" ใน Logto Console เพื่อสร้างผู้ใช้
ในแท็บ "Roles" ของรายละเอียดผู้ใช้ คุณสามารถกำหนดบทบาทให้ผู้ใช้ได้
ในตัวอย่างของเรา เราสร้างผู้ใช้ 3 คนพร้อมบทบาทดังนี้:
- Alex: Admin
- Bob: Publisher
- Charlie: Author


เพื่อการสาธิต เราสร้างทรัพยากรและการตั้งค่าเหล่านี้ผ่าน Logto Console ในโปรเจกต์จริง คุณสามารถสร้างทรัพยากรและการตั้งค่าเหล่านี้แบบโปรแกรมได้โดยใช้ Management API ที่ Logto มีให้
เชื่อมต่อ frontend ของคุณกับ Logto RBAC
ตอนนี้เราได้ตั้งค่า RBAC ใน Logto แล้ว เราสามารถเริ่มเชื่อมต่อกับ frontend ของเราได้
ก่อนอื่น ให้ทำตาม Logto Quick Starts เพื่อเชื่อมต่อ Logto กับแอปของคุณ
ในตัวอย่างนี้ เราใช้ React เพื่อสาธิต
หลังจากตั้งค่า Logto ในแอปของคุณแล้ว เราต้องเพิ่มการตั้งค่า RBAC เพื่อให้ Logto ทำงานได้
// frontend/src/App.tsx
const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // app ID ที่คุณสร้างใน Logto Console
endpoint: LOGTO_ENDPOINT, // endpoint ที่คุณสร้างใน Logto Console
resources: [API_RESOURCE], // API resource identifier ที่คุณสร้างใน Logto Console เช่น https://api.cms.com
// ขอบเขต (scopes) ทั้งหมดที่คุณอาจต้องร้องขอจาก API resource ใน frontend
scopes: [
'list:articles',
'create:articles',
'read:articles',
'update:articles',
'delete:articles',
'publish:articles',
],
};
อย่าลืมออกจากระบบและเข้าสู่ระบบใหม่เพื่อให้การเปลี่ยนแปลงนี้มีผล หากคุณเข้าสู่ระบบอยู่แล้ว
เมื่อผู้ใช้ลงชื่อเข้าใช้ด้วย Logto และร้องขอ access token สำหรับ API resources ที่ระบุไว้ข้างต้น Logto จะเพิ่ม scopes (permissions) ที่เกี่ยวข้องกับบทบาทของผู้ใช้ลงใน access token
คุณสามารถใช้ getAccessTokenClaims จาก hook useLogto เพื่อดึง scopes จาก access token ได้
// frontend/src/hooks/use-user-data.ts
import { useLogto } from '@logto/react';
import { API_RESOURCE } from '../config';
import { useState, useEffect } from 'react';
export const useUserData = () => {
const { getAccessTokenClaims } = useLogto();
const [userScopes, setUserScopes] = useState<string[]>([]);
const [userId, setUserId] = useState<string>();
useEffect(() => {
const fetchScopes = async () => {
const token = await getAccessTokenClaims(API_RESOURCE);
setUserScopes(token?.scope?.split(' ') ?? []);
setUserId(token?.sub);
};
fetchScopes();
}, [getAccessTokenClaims]);
return { userId, userScopes };
};
และคุณสามารถใช้ userScopes เพื่อตรวจสอบว่าผู้ใช้มีสิทธิ์เข้าถึงทรัพยากรหรือไม่
// frontend/src/pages/Dashboard.tsx
const Dashboard = () => {
const { userId, userScopes } = useUserData();
// ...
return (
<div>
{/* ... */}
{(userScopes.includes('delete:articles') || article.ownerId === userId) && (
<button
onClick={() => handleDelete(article.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
)}
</div>
);
};
เชื่อมต่อ backend ของคุณกับ Logto RBAC
ถึงเวลานำ Logto RBAC ไปใช้กับ backend ของคุณแล้ว
มิดเดิลแวร์การอนุญาต (Authorization) ฝั่ง backend
ก่อนอื่น เราต้องเพิ่มมิดเดิลแวร์ใน backend เพื่อตรวจสอบสิทธิ์ของผู้ใช้ ตรวจสอบว่าผู้ใช้เข้าสู่ระบบหรือไม่ และตรวจสอบว่ามีสิทธิ์เข้าถึง API ที่ต้องการหรือไม่
// backend/src/middleware/auth.js
const { createRemoteJWKSet, jwtVerify } = require('jose');
const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';
if (!authorization) {
throw new Error('Authorization header missing');
}
if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}
return authorization.slice(bearerTokenIdentifier.length + 1);
};
const hasScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const verifyJwt = async (token) => {
const JWKS = createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL));
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.LOGTO_ISSUER,
audience: process.env.LOGTO_API_RESOURCE,
});
return payload;
};
const requireAuth = (requiredScopes = []) => {
return async (req, res, next) => {
try {
// ดึง token
const token = getTokenFromHeader(req.headers);
// ตรวจสอบ token
const payload = await verifyJwt(token);
// เพิ่มข้อมูลผู้ใช้ใน request
req.user = {
id: payload.sub,
scopes: payload.scope?.split(' ') || [],
};
// ตรวจสอบสิทธิ์ที่ต้องการ
if (!hasScopes(req.user.scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
hasScopes,
};
จะเห็นว่าในมิดเดิลแวร์นี้ เราตรวจสอบว่า request จาก frontend มี access token ที่ถูกต้องหรือไม่ และตรวจสอบว่า audience ของ access token ตรงกับ API resource ที่เราสร้างใน Logto Console หรือไม่
เหตุผลที่ต้องตรวจสอบ API resource เพราะ API resource ของเราคือทรัพยากรของ backend CMS จริง ๆ และสิทธิ์ทั้งหมดของ CMS จะผูกกับ API resource นี้
เนื่องจาก API resource นี้แทนทรัพยากร CMS ใน Logto ในโค้ด frontend ของเรา เราจะต้องแนบ Access token ที่เกี่ยวข้องเมื่อเรียก API ไปยัง backend:
// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();
return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// รับ access token สำหรับ API resource
const token = await getAccessToken(API_RESOURCE);
if (!token) {
throw new ApiRequestError('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
// เพิ่ม access token ใน request headers
Authorization: `Bearer ${token}`,
...options.headers,
},
});
// ... handle response
return await response.json();
} catch (error) {
// ... error handling
}
},
[getAccessToken]
);
};
ตอนนี้เราสามารถใช้มิดเดิลแวร์ requireAuth เพื่อปกป้อง API endpoint ของเราได้
ปกป้อง API endpoint
สำหรับ API ที่ควรเข้าถึงได้เฉพาะผู้ใช้ที่มีสิทธิ์เฉพาะ เราสามารถเพิ่มข้อจำกัดในมิดเดิลแวร์ได้โดยตรง เช่น API สำหรับสร้างบทความควรเข้าถึงได้เฉพาะผู้ใช้ที่มีสิทธิ์ create:articles:
// backend/src/routes/articles.js
const { requireAuth } = require('../middleware/auth');
router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});
สำหรับ API ที่ต้องตรวจสอบทั้งสิทธิ์และความเป็นเจ้าของทรัพยากร เราสามารถใช้ฟังก์ชัน hasScopes ได้ เช่น ใน API แสดงรายการบทความ ผู้ใช้ที่มี scope list:articles จะเห็นบทความทั้งหมด ส่วนผู้เขียนจะเห็นเฉพาะบทความที่ตนเองสร้าง:
// backend/src/routes/articles.js
const { requireAuth, hasScopes } = require('../middleware/auth');
router.get('/articles', requireAuth(), async (req, res) => {
try {
// ถ้าผู้ใช้มี scope list:articles ให้แสดงบทความทั้งหมด
if (hasScopes(req.user.scopes, ['list:articles'])) {
const articles = await articleDB.list();
return res.json(articles);
}
// ไม่เช่นนั้น แสดงเฉพาะบทความของผู้ใช้
const articles = await articleDB.listByOwner(req.user.id);
res.json(articles);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch articles' });
}
});
ถึงจุดนี้ เราได้เสร็จสิ้นการใช้งาน RBAC แล้ว คุณสามารถดู ซอร์สโค้ดฉบับเต็ม เพื่อดูรายละเอียดทั้งหมด
ทดสอบการใช้งาน CMS RBAC
ตอนนี้ มาทดสอบการใช้งาน CMS RBAC ของเราด้วยผู้ใช้ 3 คนที่เพิ่งสร้างขึ้น
หากคุณพบว่าไม่สามารถเข้าสู่ระบบด้วยข้อมูลผู้ใช้ที่สร้างใน "User Management" ได้ คุณต้องเปิดใช้งานวิธีการลงชื่อเข้าใช้ที่เหมาะสมก่อน ไปที่ "Sign-in & account > Sign-up and sign-in" ใน Logto Console และเปิดใช้งานวิธีการยืนยันตัวตน (Authentication) ที่คุณต้องการ (เช่น Email + Password หรือ Username + Password)
ก่อนอื่น ลองเข้าสู่ระบบเป็น Alex และ Charles ตามลำดับและสร้างบทความ
เนื่องจาก Alex มีบทบาท Admin จึงสามารถสร้าง ลบ แก้ไข เผยแพร่ และดูบทความทั้งหมดได้

Charles ซึ่งมีบทบาท Author สามารถสร้างบทความของตนเองเท่านั้น และดู แก้ไข ลบได้เฉพาะบทความที่ตนเองเป็นเจ้าของ

Bob ซึ่งมีบทบาท Publisher สามารถดูและเผยแพร่บทความทั้งหมด แต่ไม่สามารถสร้าง แก้ไข หรือลบได้

สรุป
ขอแสดงความยินดี! คุณได้เรียนรู้วิธีใช้งานระบบ RBAC ที่แข็งแกร่งในแอปของคุณแล้ว
สำหรับกรณีที่ซับซ้อนยิ่งขึ้น เช่น การสร้างแอปแบบหลายผู้เช่า (multi-tenant) Logto มีระบบสนับสนุนองค์กร (Organization) ที่ครบถ้วน ดูคู่มือของเรา สร้างแอป SaaS แบบหลายผู้เช่า: คู่มือฉบับสมบูรณ์ตั้งแต่การออกแบบจนถึงการใช้งาน เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับการควบคุมการเข้าถึงระดับองค์กร
ขอให้สนุกกับการเขียนโค้ด! 🚀