ข้ามไปยังเนื้อหาหลัก

RBAC ในทางปฏิบัติ: การนำการอนุญาต (Authorization) ที่ปลอดภัยไปใช้กับแอปพลิเคชันของคุณ

คุณกำลังประสบปัญหาในการสร้างระบบการอนุญาต (Authorization) ที่ปลอดภัยและขยายได้สำหรับแอปพลิเคชันของคุณหรือไม่? การควบคุมการเข้าถึงตามบทบาท (RBAC) เป็นมาตรฐานอุตสาหกรรมสำหรับการจัดการสิทธิ์ของผู้ใช้ แต่การนำไปใช้อย่างถูกต้องอาจท้าทาย บทแนะนำนี้จะแสดงวิธีสร้างระบบ RBAC ที่แข็งแกร่งโดยใช้ตัวอย่างระบบจัดการเนื้อหา (CMS) ในโลกจริง

เมื่อทำตามคู่มือนี้ คุณจะได้เรียนรู้:

  • ✨ วิธีออกแบบและนำสิทธิ์แบบละเอียด (fine-grained permissions) ไปใช้ เพื่อควบคุมได้อย่างแม่นยำ
  • 🔒 แนวปฏิบัติที่ดีที่สุดในการจัดกลุ่มสิทธิ์เป็นบทบาทที่มีความหมาย
  • 👤 เทคนิคการจัดการความเป็นเจ้าของทรัพยากรอย่างมีประสิทธิภาพ
  • 🚀 วิธีทำให้ระบบการอนุญาต (Authorization) ของคุณขยายและดูแลรักษาได้ง่าย
  • 💡 ตัวอย่างการนำไปใช้จริงโดยใช้ CMS

ซอร์สโค้ดทั้งหมดของบทแนะนำนี้มีให้ที่ GitHub

ทำความเข้าใจพื้นฐานของ RBAC

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

คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ RBAC คืออะไร ได้ที่ Auth Wiki

นี่คือหลักการสำคัญที่เราจะใช้ในการนำไปใช้จริง:

การออกแบบสิทธิ์แบบละเอียด

สิทธิ์แบบละเอียด (fine-grained permissions) ช่วยให้คุณควบคุมได้อย่างแม่นยำว่าผู้ใช้สามารถทำอะไรในระบบของคุณ แทนที่จะใช้ระดับการเข้าถึงกว้างๆ เช่น "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 เราต้องพิจารณาสองประเด็นของการควบคุมการเข้าถึง:

  1. ความเป็นเจ้าของทรัพยากร - ผู้ใช้เป็นเจ้าของทรัพยากรนี้หรือไม่?
  2. สิทธิ์ตามบทบาท - บทบาทของผู้ใช้อนุญาตให้ดำเนินการนี้หรือไม่?

นี่คือวิธีที่เราจะจัดการการเข้าถึงแต่ละ 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 เพื่อความง่าย

ตั้งค่าแอปพลิเคชันของคุณ

  1. ไปที่ "Applications" ใน Logto Console เพื่อสร้างแอป React ใหม่
    • ชื่อแอปพลิเคชัน: Content Management System
    • ประเภทแอปพลิเคชัน: Traditional Web Application
    • Redirect URIs: http://localhost:5173/callback

CMS React application

กำหนดทรัพยากร API และสิทธิ์

  1. ไปที่ "API Resources" ใน Logto Console เพื่อสร้าง API resource ใหม่
    • ชื่อ API: CMS API
    • API identifier: https://api.cms.com
    • เพิ่มสิทธิ์ให้กับ API resource
      • list:articles
      • read:articles
      • create:articles
      • update:articles
      • publish:articles
      • delete:articles

CMS API resource details

สร้างบทบาท

ไปที่ Roles ใน Logto Console เพื่อสร้างบทบาทต่อไปนี้สำหรับ CMS

  • Admin
    • มีสิทธิ์ทั้งหมด
  • Publisher
    • มี read:articles, list:articles, publish:articles
  • Author
    • มี create:articles

Admin role

Publisher role

Author role

กำหนดบทบาทให้ผู้ใช้

ไปที่ส่วน "User management" ใน Logto Console เพื่อสร้างผู้ใช้

ในแท็บ "Roles" ของรายละเอียดผู้ใช้ คุณสามารถกำหนดบทบาทให้ผู้ใช้ได้

ในตัวอย่างของเรา เราสร้างผู้ใช้ 3 คนพร้อมบทบาทดังนี้:

  • Alex: Admin
  • Bob: Publisher
  • Charlie: Author

User management

User details - Alex

บันทึก:

เพื่อการสาธิต เราสร้างทรัพยากรและการตั้งค่าเหล่านี้ผ่าน 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 ครบถ้วนแล้ว คุณสามารถดู ซอร์สโค้ดฉบับเต็ม เพื่อดูการนำไปใช้ทั้งหมด

ทดสอบการนำ RBAC ไปใช้กับ CMS

ตอนนี้ มาทดสอบการนำ RBAC ไปใช้กับ CMS โดยใช้ผู้ใช้ 3 คนที่เราสร้างไว้

บันทึก:

หากคุณพบว่าไม่สามารถเข้าสู่ระบบด้วยข้อมูลผู้ใช้ที่สร้างใน "User Management" ได้ คุณต้องเปิดใช้งานวิธีการลงชื่อเข้าใช้ที่เหมาะสมก่อน ไปที่ "Sign-in Experience" ใน Logto Console และเปิดใช้งานวิธีการยืนยันตัวตนที่คุณต้องการ (เช่น Email + Password หรือ Username + Password)

ก่อนอื่น ลองเข้าสู่ระบบเป็น Alex และ Charles ตามลำดับและสร้างบทความ

เนื่องจาก Alex มีบทบาท Admin จึงสามารถสร้าง ลบ แก้ไข เผยแพร่ และดูบทความทั้งหมดได้

CMS dashboard - Alex

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

CMS dashboard - Charles - Article list

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

CMS dashboard - Bob

สรุป

ขอแสดงความยินดี! คุณได้เรียนรู้วิธีนำระบบ RBAC ที่แข็งแกร่งไปใช้ในแอปพลิเคชันของคุณแล้ว

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

ขอให้สนุกกับการเขียนโค้ด! 🚀