1. Authentication vs Authorization — ความแตกต่างที่ต้องรู้
Authentication (AuthN) คือกระบวนการ "พิสูจน์ตัวตน" — ตรวจสอบว่าผู้ใช้เป็นใคร เช่น การ Login ด้วย Email และ Password, ใช้ Google OAuth, หรือสแกน Face ID ผลลัพธ์คือระบบรู้ว่า "ผู้ใช้คนนี้คือ user@example.com"
Authorization (AuthZ) คือกระบวนการ "ตรวจสอบสิทธิ์" — หลังจากรู้แล้วว่าเป็นใคร ระบบจะตัดสินว่าผู้ใช้คนนั้น "ทำอะไรได้บ้าง" เช่น User ทั่วไปดูข้อมูลได้แต่แก้ไขไม่ได้, Admin แก้ไขและลบได้, เจ้าของข้อมูลเข้าถึงได้เฉพาะข้อมูลของตัวเอง
ทั้งสองต้องทำงานร่วมกันเสมอ: ต้องพิสูจน์ตัวตน (AuthN) ก่อน จึงจะตรวจสอบสิทธิ์ (AuthZ) ได้ ในหลักสูตรนี้ใช้ Supabase Auth สำหรับ AuthN และ Row Level Security (RLS) สำหรับ AuthZ
Authentication (AuthN) Authorization (AuthZ)
──────────────────────── ─────────────────────────────
"คุณเป็นใคร?" "คุณทำอะไรได้บ้าง?"
• Login ด้วย Email + Password • ดูได้เฉพาะ bookings ของตัวเอง
• OAuth (Google, GitHub) • Admin เท่านั้นที่ลบ service ได้
• Magic Link (ลิงก์ทาง Email) • RLS policy บน Supabase Table
ผลลัพธ์: JWT token + session ผลลัพธ์: allow / deny operation
2. Supabase Auth — Email/Password, Magic Link, OAuth
Supabase Auth เป็น Authentication Service ที่มาพร้อม Supabase โดยไม่ต้องสร้างระบบ Auth เอง รองรับหลายวิธีการ Login:
-
Email + Password — วิธีคลาสสิก ผู้ใช้สมัครด้วย Email และรหัสผ่าน
Supabase จะ Hash รหัสผ่านให้อัตโนมัติและเก็บใน
auth.userstable ที่ซ่อนอยู่ - Magic Link — ส่งลิงก์ Login ทาง Email โดยไม่ต้องใช้รหัสผ่าน ผู้ใช้คลิกลิงก์ → ระบบ Login อัตโนมัติ ปลอดภัยและใช้งานง่าย
- OAuth (Social Login) — Login ผ่าน Provider ภายนอก เช่น Google, GitHub, Facebook ผู้ใช้ไม่ต้องสร้าง account ใหม่ ใช้ account ที่มีอยู่แล้วได้เลย
ข้อมูลผู้ใช้ทั้งหมดเก็บอยู่ใน Schema พิเศษชื่อ auth ใน Supabase
ซึ่งไม่สามารถเข้าถึงได้โดยตรงจาก frontend เพื่อความปลอดภัย
Supabase มี auth.uid() function ให้ใช้ใน RLS Policy เพื่อดึง ID ของผู้ใช้ปัจจุบัน
สำหรับแอปที่ผู้ใช้คุ้นเคยกับ Email/Password ให้ใช้แบบแรก ถ้าต้องการลดขั้นตอนให้ผู้ใช้ ใช้ Magic Link (ไม่ต้องจำรหัสผ่าน) สำหรับแอปที่กลุ่มผู้ใช้มี Google Account ทุกคน OAuth จะสะดวกที่สุด
3. JWT Token และ Session Management
เมื่อ Login สำเร็จ Supabase จะออก JWT (JSON Web Token) ให้
JWT คือ String ที่เข้ารหัสข้อมูลผู้ใช้ (เช่น user_id, email, role)
และลงลายเซ็น (sign) ด้วย Secret Key ของ Supabase
ฝั่ง Server สามารถตรวจสอบความถูกต้องของ JWT ได้โดยไม่ต้องถามฐานข้อมูล
Supabase JS Client จัดการ Session ให้อัตโนมัติ: เก็บ JWT ใน localStorage,
ส่ง Token ในทุก Request ไปยัง Supabase API, และ refresh Token อัตโนมัติก่อนหมดอายุ
นักพัฒนาไม่ต้องเขียน logic เหล่านี้เอง
supabase.auth.onAuthStateChange() เป็น Event Listener ที่แจ้งเตือนเมื่อ Auth state เปลี่ยน
เช่น เมื่อ Login, Logout, หรือ Token refresh ใช้สร้าง AuthContext
เพื่อให้ทุก Component ในแอปรับรู้สถานะ Auth โดยอัตโนมัติ
Login สำเร็จ
│
▼
Supabase ออก JWT (access_token + refresh_token)
│
▼
Supabase JS Client เก็บ JWT ใน localStorage
│
├── onAuthStateChange('SIGNED_IN', session) ถูกเรียก
│ │
│ ▼
│ AuthContext อัปเดต user state → UI เปลี่ยน (แสดงชื่อผู้ใช้, logout button)
│
└── ทุก Supabase API call ส่ง JWT ใน Authorization header อัตโนมัติ
│
▼
RLS ตรวจสอบ auth.uid() → อนุญาตเฉพาะข้อมูลของตัวเอง
4. supabase.auth API — signUp, signInWithPassword, signOut, getSession
Supabase JS Client มี API หลักสำหรับ Authentication ดังนี้:
-
supabase.auth.signUp({ email, password, options })— สมัครสมาชิกใหม่ ถ้าเปิดใช้ Email Confirmation ผู้ใช้ต้องยืนยันอีเมลก่อน Login ได้options.dataใช้เก็บข้อมูลเพิ่มเติมเช่นfull_name -
supabase.auth.signInWithPassword({ email, password })— Login ด้วย Email และรหัสผ่าน คืนค่า{ data: { user, session }, error } -
supabase.auth.signOut()— ออกจากระบบ ล้าง Session ใน localStorage และ triggeronAuthStateChange('SIGNED_OUT') -
supabase.auth.getSession()— ดู session ปัจจุบัน ใช้ตรวจสอบว่า Login อยู่หรือไม่ เมื่อแอปเริ่มต้น ต้องเรียก getSession ก่อนเพื่อกู้ session จาก localStorage
ทุก API คืน { data, error } pattern เสมอ ต้องตรวจสอบ error
ก่อนใช้ data ถ้า error ไม่ใช่ null แสดงว่าเกิดข้อผิดพลาด เช่น
รหัสผ่านไม่ถูกต้อง, Email ซ้ำ, หรือ Email ไม่ได้ยืนยัน
5. Protected Route Pattern ใน TanStack Router
Protected Route คือ Route ที่เข้าถึงได้เฉพาะผู้ใช้ที่ Login แล้ว ถ้ายังไม่ Login ให้ redirect ไปหน้า Login โดยอัตโนมัติ เป็น pattern สำคัญในทุกแอปที่มีระบบสมาชิก
วิธีทำใน React + TanStack Router คือสร้าง Component ชื่อ ProtectedRoute
ที่ตรวจสอบ user จาก useAuth() hook:
ถ้า loading แสดง spinner, ถ้าไม่มี user ให้ navigate ไป /login,
ถ้ามี user ให้ render children ตามปกติ
จากนั้นห่อ Component ที่ต้องการปกป้องด้วย <ProtectedRoute>
เช่น <ProtectedRoute><BookingPage /></ProtectedRoute>
วิธีนี้แยก logic การตรวจสอบ Auth ออกจาก Business Logic ของหน้านั้น ๆ ทำให้โค้ดสะอาดและ reuse ได้
Protected Route ใน Frontend เป็นแค่ UX — ป้องกันผู้ใช้ทั่วไปไม่ให้เห็นหน้าที่ไม่ควรเห็น แต่ไม่ได้ป้องกัน API ถ้าใครเปิด DevTools แล้วเรียก API โดยตรงก็ยังได้ ความปลอดภัยที่แท้จริงต้องมาจาก RLS บน Supabase ซึ่งทำงานฝั่ง Database
6. Row Level Security (RLS) — Authorization ระดับแถวข้อมูล
Row Level Security (RLS) คือฟีเจอร์ของ PostgreSQL (ฐานข้อมูลที่ Supabase ใช้) ที่กำหนดว่าผู้ใช้แต่ละคน SELECT/INSERT/UPDATE/DELETE แถวข้อมูลไหนได้บ้าง ทำงานโดยอัตโนมัติในทุก Query ไม่ต้องเขียน WHERE clause เพิ่มเองในโค้ด
เมื่อเปิด RLS บน Table ค่า default คือ "ไม่มีใครเข้าถึงได้เลย" จนกว่าจะสร้าง Policy
Policy คือกฎที่ระบุเงื่อนไขว่าแถวข้อมูลนั้นควรแสดงให้ผู้ใช้คนใดเห็น
ตัวอย่างเช่น: "แสดงเฉพาะแถวที่ user_id = auth.uid()"
ซึ่งหมายความว่าผู้ใช้จะเห็นเฉพาะข้อมูลของตัวเองเท่านั้น
การสร้าง RLS Policy ทำได้ใน Supabase Dashboard (Table Editor → RLS → New Policy)
หรือใช้ SQL: CREATE POLICY "policy_name" ON table_name FOR SELECT USING (auth.uid() = user_id);
auth.uid() คือ function พิเศษที่คืน UUID ของผู้ใช้ปัจจุบันจาก JWT
ไม่มี RLS: มี RLS + Policy:
───────────────────── ──────────────────────────────────
SELECT * FROM bookings SELECT * FROM bookings
→ ได้ทุกแถวของทุกคน → ได้เฉพาะแถวที่ user_id = auth.uid()
User A เห็น booking User A เห็น booking ของ A เท่านั้น
ของ User B, C, D ด้วย User B เห็น booking ของ B เท่านั้น
⚠️ ความเสี่ยงสูงมาก ✓ ปลอดภัยระดับ Database
Table ใหม่ใน Supabase มี RLS ปิดอยู่ by default ซึ่งหมายความว่าใครก็สามารถดึงข้อมูลได้ ควร Enable RLS พร้อมกับสร้าง Policy ทันทีหลังสร้าง Table ก่อนที่จะมีข้อมูลจริงเข้ามา
// snippet 1: Supabase Auth operations
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// สมัครสมาชิก
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: { data: { full_name: 'สมชาย ใจดี' } },
});
// เข้าสู่ระบบ
const { data: { user, session }, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123',
});
// ออกจากระบบ
await supabase.auth.signOut();
// ดู session ปัจจุบัน
const { data: { session } } = await supabase.auth.getSession();
console.log(session?.user?.email);
// ฟัง auth state changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') console.log('เข้าสู่ระบบแล้ว:', session.user.email);
if (event === 'SIGNED_OUT') console.log('ออกจากระบบแล้ว');
});
// snippet 2: Auth Context + useAuth hook
import { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
setLoading(false);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_, session) => setUser(session?.user ?? null)
);
return () => subscription.unsubscribe();
}, []);
return (
<AuthContext.Provider value={{ user, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
// snippet 3: Protected Route + Login Form
import { useAuth } from '../contexts/AuthContext';
import { useNavigate } from '@tanstack/react-router';
// Protected Route wrapper
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const navigate = useNavigate();
if (loading) return <p>กำลังตรวจสอบ...</p>;
if (!user) {
navigate({ to: '/login' });
return null;
}
return children;
}
// Login Form
function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) { setError(error.message); return; }
navigate({ to: '/' });
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="อีเมล" required />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="รหัสผ่าน" required />
{error && <p className="error">{error}</p>}
<button type="submit">เข้าสู่ระบบ</button>
</form>
);
}
🧪 ปฏิบัติการ Lab 8 — BookEasy: ระบบ Login/Register
เพิ่ม Authentication ให้ BookEasy ด้วย Supabase Auth ตั้งแต่สร้าง AuthContext, หน้า Login/Register ไปจนถึงปกป้อง route /booking ด้วย ProtectedRoute และตั้ง RLS บน Supabase
ต่อยอดจาก Lab 7 — เพิ่ม Authentication เพื่อให้ผู้ใช้ต้อง Login ก่อนจอง
ติดตั้ง Supabase JS Client และสร้าง AuthContext เพื่อให้ทุก Component เข้าถึงสถานะ Auth ได้
- รัน
npm install @supabase/supabase-jsใน folderbookeasy(frontend) - สร้างไฟล์
src/lib/supabase.js— สร้าง Supabase client ด้วย Public URL และ anon key จาก Supabase Dashboard (Settings → API) - สร้างไฟล์
src/contexts/AuthContext.jsxตาม snippet 2 ด้านบน (AuthProvider + useAuth hook) - เปิด
src/main.jsxแล้วห่อ<RouterProvider />ด้วย<AuthProvider>
import { createClient } from '@supabase/supabase-js';
const SUPABASE_URL = 'https://xxxxxxxxxxxx.supabase.co';
const SUPABASE_ANON_KEY = 'eyJ...'; // anon public key
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
@supabase/supabase-js อยู่ใน package.json
ไฟล์ src/lib/supabase.js และ src/contexts/AuthContext.jsx มีอยู่
main.jsx ห่อ RouterProvider ด้วย AuthProvider แล้ว
แอปยังรันได้โดยไม่มี Error ใน console
อย่าเก็บ API key ใน repository สาธารณะ ใช้ .env file แทน:
สร้าง .env ที่ root ของ bookeasy ใส่ VITE_SUPABASE_URL และ VITE_SUPABASE_ANON_KEY
แล้วอ่านใน supabase.js ด้วย import.meta.env.VITE_SUPABASE_URL
สร้างหน้า Login และ Register พร้อมเพิ่ม route ใน router
-
สร้าง
src/pages/LoginPage.jsx— Form มี Email + Password + ปุ่ม "เข้าสู่ระบบ" เรียกsupabase.auth.signInWithPassword()และแสดง error ถ้าล้มเหลว Login สำเร็จให้ navigate ไป/ -
สร้าง
src/pages/RegisterPage.jsx— Form มี Email + Password + Full Name เรียกsupabase.auth.signUp()และแสดงข้อความสำเร็จหรือ error -
เพิ่ม routes
/loginและ/registerในsrc/router.js
import { useState } from 'react';
import { supabase } from '../lib/supabase';
import { useNavigate } from '@tanstack/react-router';
export function RegisterPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [fullName, setFullName] = useState('');
const [message, setMessage] = useState('');
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
const { error } = await supabase.auth.signUp({
email,
password,
options: { data: { full_name: fullName } },
});
if (error) { setMessage(error.message); return; }
setMessage('สมัครสมาชิกสำเร็จ! กรุณาตรวจสอบอีเมลเพื่อยืนยัน');
}
return (
<form onSubmit={handleSubmit}>
<input type="text" value={fullName} onChange={e => setFullName(e.target.value)} placeholder="ชื่อ-นามสกุล" required />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="อีเมล" required />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="รหัสผ่าน (≥6 ตัว)" required minLength={6} />
{message && <p>{message}</p>}
<button type="submit">สมัครสมาชิก</button>
<a href="/login">มีบัญชีอยู่แล้ว? เข้าสู่ระบบ</a>
</form>
);
}
เปิด /register → กรอกข้อมูล → กด Submit → แสดงข้อความสำเร็จ
เปิด Supabase Dashboard → Authentication → Users → เห็น user ใหม่
เปิด /login → Login ด้วย account ที่สมัคร → navigate ไปหน้าแรก
Login ผิด → แสดง error message
ถ้า Supabase Project เปิด "Email Confirmation" ไว้ ผู้ใช้ต้องคลิก confirm link ในอีเมลก่อน Login ได้ ระหว่างพัฒนาสามารถปิดฟีเจอร์นี้ได้ที่ Authentication → Settings → Confirm email
ห่อ BookingForm ด้วย ProtectedRoute และเพิ่มปุ่ม Login/Logout ใน Header
-
สร้าง
src/components/ProtectedRoute.jsxตาม snippet 3 ด้านบน ตรวจสอบuserจากuseAuth()— redirect ไป/loginถ้าไม่มี user -
ใน
src/router.jsหรือไฟล์ที่กำหนด route/bookingห่อ Component ด้วย<ProtectedRoute> -
เปิด
src/components/Header.jsx(หรือ Navbar): ใช้useAuth()ตรวจสอบuserถ้ามี user แสดง "สวัสดี [ชื่อ]" + ปุ่ม "ออกจากระบบ" ที่เรียกsupabase.auth.signOut()ถ้าไม่มี user แสดงปุ่ม "เข้าสู่ระบบ" ที่ navigate ไป/login
import { useAuth } from '../contexts/AuthContext';
import { useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
export function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && !user) {
navigate({ to: '/login' });
}
}, [user, loading, navigate]);
if (loading) return <p>กำลังตรวจสอบ...</p>;
if (!user) return null;
return children;
}
เปิด /booking โดยไม่ Login → redirect ไป /login ทันที
Login แล้วเปิด /booking → เห็นหน้า BookingForm ตามปกติ
Header แสดงปุ่ม "เข้าสู่ระบบ" เมื่อยังไม่ Login และแสดงชื่อผู้ใช้ + ปุ่ม "ออกจากระบบ" เมื่อ Login แล้ว
กด "ออกจากระบบ" → กลับสู่สถานะไม่ Login
ใช้ useEffect สำหรับ navigate เพราะการเรียก navigate()
ระหว่าง render โดยตรงอาจทำให้เกิด React warning
ตรวจสอบ loading ก่อนเสมอ ไม่เช่นนั้นอาจ redirect ก่อนที่ session จะโหลดเสร็จ
เปิด Row Level Security บน bookings table และสร้าง Policy ให้ผู้ใช้เห็นเฉพาะข้อมูลของตัวเอง
- เปิด Supabase Dashboard → Table Editor → เลือก
bookingstable - คลิก "RLS disabled" เพื่อ Enable RLS → ยืนยัน
- คลิก "New Policy" → เลือก template "Enable read access for users based on user_id"
- ตรวจสอบว่า Policy ใช้
auth.uid() = user_idเป็น USING expression - สร้าง Policy แยกสำหรับ INSERT ที่กำหนด
user_idอัตโนมัติเป็นauth.uid()
-- รัน SQL นี้ใน Supabase SQL Editor
-- เปิด RLS
ALTER TABLE bookings ENABLE ROW LEVEL SECURITY;
-- Policy: ดูได้เฉพาะ bookings ของตัวเอง
CREATE POLICY "users can view own bookings"
ON bookings FOR SELECT
USING (auth.uid() = user_id);
-- Policy: จองได้เฉพาะในนามตัวเอง
CREATE POLICY "users can insert own bookings"
ON bookings FOR INSERT
WITH CHECK (auth.uid() = user_id);
Supabase Dashboard แสดงว่า RLS เปิดอยู่บน bookings table
Login ด้วย User A → จอง → เห็นเฉพาะ bookings ของ User A เท่านั้น
Login ด้วย User B → เห็นเฉพาะ bookings ของ User B ไม่เห็นของ User A
ทดสอบด้วย Supabase SQL Editor: SELECT * FROM bookings (ไม่มี session) → ได้ 0 แถว
ต้องเพิ่ม column user_id UUID REFERENCES auth.users(id) ใน bookings table ถ้ายังไม่มี
และใน BookingForm ส่ง user_id: user.id ไปด้วยเมื่อ INSERT
ดึง user จาก useAuth() hook ภายใน BookingForm