Qwik Framework และ Resumability คืออะไร
Qwik เป็น JavaScript framework ที่ใช้แนวคิด Resumability แทน Hydration ที่ framework อื่นเช่น React, Vue, Svelte ใช้ ปกติ SSR frameworks ต้อง hydrate คือดาวน์โหลด JavaScript ทั้งหมดมาแล้ว replay logic เพื่อให้ page interactive ได้ แต่ Qwik ใช้ Resumability ที่ serialize state ทั้งหมดไว้ใน HTML ทำให้ browser สามารถ resume จากจุดที่ server หยุดไว้ได้ทันทีโดยไม่ต้องดาวน์โหลด JavaScript ล่วงหน้า
ผลลัพธ์คือ Qwik apps มี Time to Interactive (TTI) ที่เร็วมากเกือบเท่ากับ First Contentful Paint (FCP) ไม่ว่า app จะใหญ่แค่ไหน เพราะ JavaScript จะถูกโหลดเฉพาะเมื่อผู้ใช้ interact กับส่วันนี้ั้นจริงๆ เรียกว่า fine-grained lazy loading
RBAC (Role-Based Access Control) เป็นระบบควบคุมสิทธิ์โดยกำหนด roles ให้ผู้ใช้ เช่น admin, editor, viewer แต่ละ role มี permissions ที่กำหนดไว้ ส่วน ABAC (Attribute-Based Access Control) เป็นระบบที่ยืดหยุ่นกว่าโดยตัดสินสิทธิ์จาก attributes ของ user, resource, action และ environment เช่น อนุญาตเฉพาะเมื่อ user.department == resource.department และ time อยู่ในเวลาทำการ
การรวม Qwik กับ RBAC/ABAC ช่วยสร้าง web application ที่ทั้งเร็วและปลอดภัย โดย Qwik จัดการ frontend performance ส่วน RBAC/ABAC จัดการ authorization ทั้งฝั่ง server และ client
ติดตั้ง Qwik และสร้างโปรเจกต์แรก
เริ่มต้นโปรเจกต์ Qwik พร้อม QwikCity
# สร้างโปรเจกต์ Qwik ใหม่
npm create qwik@latest
# เลือก: Basic App (QwikCity)
# ชื่อโปรเจกต์: qwik-rbac-app
cd qwik-rbac-app
npm install
# ติดตั้ง dependencies เพิ่มเติม
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
# โครงสร้างโปรเจกต์
# qwik-rbac-app/
# ├── src/
# │ ├── components/ # UI components
# │ ├── routes/ # Pages (file-based routing)
# │ │ ├── index.tsx
# │ │ ├── admin/
# │ │ │ └── index.tsx
# │ │ ├── dashboard/
# │ │ │ └── index.tsx
# │ │ └── api/
# │ │ ├── auth/
# │ │ │ └── login/index.ts
# │ │ └── users/index.ts
# │ ├── lib/
# │ │ ├── auth.ts # Authentication logic
# │ │ ├── rbac.ts # RBAC implementation
# │ │ └── abac.ts # ABAC implementation
# │ └── root.tsx
# ├── package.json
# └── vite.config.ts
# รัน development server
npm run dev
# Server: http://localhost:5173
# Build สำหรับ production
npm run build
npm run serve
RBAC และ ABAC ต่างกันอย่างไร
เปรียบเทียบ RBAC กับ ABAC แบบละเอียด
# เปรียบเทียบ RBAC vs ABAC
#
# === RBAC (Role-Based Access Control) ===
# หลักการ: กำหนด roles → แต่ละ role มี permissions
# ตัวอย่าง:
# admin → [create, read, update, delete, manage_users]
# editor → [create, read, update]
# viewer → [read]
#
# ข้อดี:
# - เข้าใจง่าย จัดการง่าย
# - เหมาะสำหรับ org ขนาดเล็ก-กลาง
# - Performance ดี (lookup เร็ว)
#
# ข้อเสีย:
# - Role Explosion: เมื่อ org ใหญ่ขึ้น roles เยอะมาก
# - ไม่ยืดหยุ่น: ไม่สามารถกำหนดเงื่อนไขซับซ้อนได้
# - ไม่รองรับ context: ไม่พิจารณาเวลา สถานที่ resource owner
#
# === ABAC (Attribute-Based Access Control) ===
# หลักการ: ตัดสินจาก attributes + policies
# ตัวอย่าง:
# Policy: ALLOW if
# user.role == "editor" AND
# resource.department == user.department AND
# action == "update" AND
# environment.time BETWEEN "09:00" AND "18:00"
#
# ข้อดี:
# - ยืดหยุ่นมาก กำหนดเงื่อนไขซับซ้อนได้
# - ไม่มี Role Explosion
# - รองรับ context (เวลา สถานที่ device)
# - Fine-grained control ถึงระดับ resource
#
# ข้อเสีย:
# - ซับซ้อนกว่า RBAC มาก
# - Performance อาจช้ากว่า (evaluate policies)
# - Debug ยากกว่า
#
# === Hybrid RBAC + ABAC ===
# ใช้ RBAC เป็นฐาน + ABAC สำหรับเงื่อนไขพิเศษ
# ตัวอย่าง:
# ขั้นที่ 1: ตรวจ RBAC → user มี role "editor" ไหม?
# ขั้นที่ 2: ตรวจ ABAC → resource อยู่ใน department เดียวกันไหม?
# ขั้นที่ 3: ตรวจ context → อยู่ในเวลาทำการไหม?
สร้างระบบ RBAC ด้วย Qwik
implement RBAC system สำหรับ Qwik application
// src/lib/rbac.ts — RBAC Implementation
export type Role = "admin" | "editor" | "viewer" | "moderator";
export type Permission =
| "create_post"
| "read_post"
| "update_post"
| "delete_post"
| "manage_users"
| "view_analytics"
| "moderate_comments";
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
admin: [
"create_post", "read_post", "update_post", "delete_post",
"manage_users", "view_analytics", "moderate_comments",
],
editor: [
"create_post", "read_post", "update_post",
"view_analytics",
],
moderator: [
"read_post", "moderate_comments", "view_analytics",
],
viewer: ["read_post"],
};
export interface User {
id: string;
email: string;
name: string;
roles: Role[];
}
export function hasPermission(user: User, permission: Permission): boolean {
return user.roles.some((role) =>
ROLE_PERMISSIONS[role]?.includes(permission)
);
}
export function hasRole(user: User, role: Role): boolean {
return user.roles.includes(role);
}
export function hasAnyRole(user: User, roles: Role[]): boolean {
return roles.some((role) => user.roles.includes(role));
}
export function getUserPermissions(user: User): Permission[] {
const permissions = new Set<Permission>();
for (const role of user.roles) {
for (const perm of ROLE_PERMISSIONS[role] || []) {
permissions.add(perm);
}
}
return Array.from(permissions);
}
// src/routes/api/auth/login/index.ts — Login API
import { type RequestHandler } from "@builder.io/qwik-city";
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const USERS_DB: Record<string, { password: string; user: User }> = {
"admin@example.com": {
password: "$2b$10$hashedpassword",
user: { id: "1", email: "admin@example.com", name: "Admin", roles: ["admin"] },
},
"editor@example.com": {
password: "$2b$10$hashedpassword",
user: { id: "2", email: "editor@example.com", name: "Editor", roles: ["editor"] },
},
};
export const onPost: RequestHandler = async ({ json, parseBody }) => {
const body = await parseBody();
const { email, password } = body as { email: string; password: string };
const record = USERS_DB[email];
if (!record) {
json(401, { error: "Invalid credentials" });
return;
}
const token = jwt.sign(
{ sub: record.user.id, email, roles: record.user.roles },
JWT_SECRET,
{ expiresIn: "24h" }
);
json(200, { token, user: record.user });
};
// src/components/ProtectedRoute.tsx — Route Guard
import { component$, Slot } from "@builder.io/qwik";
import { useAuthSession } from "~/lib/auth-context";
import type { Permission } from "~/lib/rbac";
import { hasPermission } from "~/lib/rbac";
interface Props {
permission: Permission;
fallback?: string;
}
export const ProtectedRoute = component$<Props>(({ permission, fallback }) => {
const session = useAuthSession();
if (!session.value?.user) {
return <div>Please login to access this page.</div>;
}
if (!hasPermission(session.value.user, permission)) {
return <div>{fallback || "You do not have permission to view this page."}</div>;
}
return <Slot />;
});
สร้างระบบ ABAC แบบ Policy-Based
implement ABAC engine สำหรับ fine-grained access control
// src/lib/abac.ts — ABAC Policy Engine
export interface PolicyContext {
user: {
id: string;
roles: string[];
department: string;
clearanceLevel: number;
};
resource: {
id: string;
type: string;
owner: string;
department: string;
classification: string;
};
action: string;
environment: {
time: Date;
ipAddress: string;
userAgent: string;
};
}
export interface Policy {
name: string;
description: string;
effect: "allow" | "deny";
condition: (ctx: PolicyContext) => boolean;
priority: number;
}
const policies: Policy[] = [
{
name: "admin-full-access",
description: "Admin has full access to everything",
effect: "allow",
priority: 100,
condition: (ctx) => ctx.user.roles.includes("admin"),
},
{
name: "owner-manage-own-resources",
description: "Users can manage their own resources",
effect: "allow",
priority: 90,
condition: (ctx) =>
ctx.resource.owner === ctx.user.id &&
["read", "update", "delete"].includes(ctx.action),
},
{
name: "same-department-read",
description: "Users can read resources in their department",
effect: "allow",
priority: 80,
condition: (ctx) =>
ctx.action === "read" &&
ctx.resource.department === ctx.user.department,
},
{
name: "editor-create-in-department",
description: "Editors can create resources in their department",
effect: "allow",
priority: 70,
condition: (ctx) =>
ctx.user.roles.includes("editor") &&
ctx.action === "create" &&
ctx.resource.department === ctx.user.department,
},
{
name: "business-hours-only",
description: "Deny write operations outside business hours",
effect: "deny",
priority: 200,
condition: (ctx) => {
const hour = ctx.environment.time.getHours();
const isBusinessHours = hour >= 9 && hour < 18;
const isWeekday = ctx.environment.time.getDay() >= 1 &&
ctx.environment.time.getDay() <= 5;
return !isBusinessHours || !isWeekday;
},
},
{
name: "classified-clearance-check",
description: "Only high clearance users can access classified resources",
effect: "deny",
priority: 150,
condition: (ctx) =>
ctx.resource.classification === "classified" &&
ctx.user.clearanceLevel < 3,
},
];
export class ABACEngine {
private policies: Policy[];
constructor(customPolicies?: Policy[]) {
this.policies = customPolicies || policies;
}
evaluate(context: PolicyContext): { allowed: boolean; reason: string } {
// Sort by priority (higher = evaluated first)
const sorted = [...this.policies].sort((a, b) => b.priority - a.priority);
for (const policy of sorted) {
if (policy.condition(context)) {
if (policy.effect === "deny") {
return { allowed: false, reason: `Denied by: ` };
}
}
}
// Check allow policies
for (const policy of sorted) {
if (policy.effect === "allow" && policy.condition(context)) {
return { allowed: true, reason: `Allowed by: ` };
}
}
// Default deny
return { allowed: false, reason: "No matching allow policy" };
}
addPolicy(policy: Policy): void {
this.policies.push(policy);
}
}
// Usage
const engine = new ABACEngine();
const result = engine.evaluate({
user: { id: "1", roles: ["editor"], department: "marketing", clearanceLevel: 2 },
resource: { id: "post-1", type: "post", owner: "2", department: "marketing", classification: "internal" },
action: "update",
environment: { time: new Date(), ipAddress: "192.168.1.1", userAgent: "Chrome" },
});
// result: { allowed: false, reason: "Denied by: business-hours-only" } (ถ้านอกเวลาทำการ)
รวม RBAC ABAC เข้ากับ Qwik Middleware
สร้าง middleware สำหรับตรวจสอบ authorization ใน Qwik routes
// src/routes/layout.tsx — Qwik Middleware สำหรับ Auth
import { component$, Slot } from "@builder.io/qwik";
import { routeLoader$, type RequestHandler } from "@builder.io/qwik-city";
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
export const onRequest: RequestHandler = async ({ cookie, sharedMap, redirect, url }) => {
const token = cookie.get("auth_token")?.value;
const publicPaths = ["/", "/login", "/api/auth/login"];
if (publicPaths.includes(url.pathname)) return;
if (!token) {
throw redirect(302, "/login");
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as {
sub: string; email: string; roles: string[];
};
sharedMap.set("user", decoded);
} catch {
cookie.delete("auth_token");
throw redirect(302, "/login");
}
};
export const useUser = routeLoader$(({ sharedMap }) => {
return sharedMap.get("user") || null;
});
export default component$(() => {
return <Slot />;
});
// src/routes/admin/index.tsx — Admin Route with RBAC
import { component$ } from "@builder.io/qwik";
import { routeLoader$, type RequestHandler } from "@builder.io/qwik-city";
export const onRequest: RequestHandler = async ({ sharedMap, error }) => {
const user = sharedMap.get("user");
if (!user?.roles?.includes("admin")) {
throw error(403, "Admin access required");
}
};
export const useAdminData = routeLoader$(async () => {
return {
totalUsers: 150,
totalPosts: 1200,
activeUsers: 45,
};
});
export default component$(() => {
const data = useAdminData();
return (
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Admin Dashboard</h1>
<div class="grid grid-cols-3 gap-4">
<div class="bg-blue-100 p-4 rounded">
<p class="text-sm">Total Users</p>
<p class="text-3xl font-bold">{data.value.totalUsers}</p>
</div>
<div class="bg-green-100 p-4 rounded">
<p class="text-sm">Total Posts</p>
<p class="text-3xl font-bold">{data.value.totalPosts}</p>
</div>
<div class="bg-yellow-100 p-4 rounded">
<p class="text-sm">Active Users</p>
<p class="text-3xl font-bold">{data.value.activeUsers}</p>
</div>
</div>
</div>
);
});
FAQ คำถามที่พบบ่อย
Q: Qwik Resumability ดีกว่า React Hydration อย่างไร?
A: React ต้อง hydrate คือดาวน์โหลด JavaScript ทั้งหมดและ replay logic เพื่อทำให้ page interactive ยิ่ง app ใหญ่ hydration ยิ่งช้า Qwik ไม่ต้อง hydrate เลย serialize state ทั้งหมดใน HTML แล้ว JavaScript จะโหลดเฉพาะส่วนที่ผู้ใช้ interact จริงๆ ทำให้ TTI เร็วมากไม่ว่า app จะใหญ่แค่ไหน
Q: ควรใช้ RBAC หรือ ABAC?
A: ถ้า permission model ไม่ซับซ้อนมาก (roles น้อยกว่า 10) ใช้ RBAC เพราะง่ายกว่า ถ้าต้องการ fine-grained control เช่น จำกัดตามเวลา department resource ownership ใช้ ABAC หรือ Hybrid RBAC+ABAC ที่ใช้ RBAC เป็นฐานแล้วเพิ่ม ABAC policies สำหรับเงื่อนไขพิเศษ
Q: Qwik production-ready แล้วหรือยัง?
A: Qwik เปิดตัว v1.0 stable แล้วและมีบริษัทหลายแห่งใช้ใน production มี QwikCity สำหรับ full-stack development มี adapter สำหรับ deploy บน Cloudflare Pages, Vercel, Netlify และ Node.js server มี community ที่เติบโตเร็วและมี documentation ที่ดี
Q: Authorization ควรตรวจสอบที่ frontend หรือ backend?
A: ต้องตรวจสอบที่ backend เสมอเพราะ frontend สามารถถูก bypass ได้ frontend ใช้สำหรับซ่อน UI elements ที่ผู้ใช้ไม่มีสิทธิ์เพื่อ UX ที่ดี แต่ทุก API request ต้องตรวจสอบ authorization ที่ server side ด้วย ใน Qwik ใช้ routeLoader$ และ onRequest handler สำหรับตรวจสอบฝั่ง server
Q: JWT Token เก็บที่ไหนปลอดภัยที่สุด?
A: เก็บใน httpOnly cookie ที่มี secure flag และ sameSite=strict เป็นวิธีที่ปลอดภัยที่สุดสำหรับ web app เพราะ JavaScript เข้าถึง httpOnly cookie ไม่ได้ ป้องกัน XSS ส่วน localStorage ไม่แนะนำเพราะ JavaScript ทุกตัวใน page เข้าถึงได้ ถ้าโดน XSS token จะถูกขโมย
