01204223/authentication
- ส่วนหนึ่งของวิชา 01204223
ในโครงงานของเรา เราจะใช้การทำ authentication ด้วย Token อย่างไรก็ตาม ยังมีวิธีที่ทำได้อีกหลายวิธี ด้านล่างเป็นความรู้พื้นฐานที่อ่านประกอบได้
แนวทางการทำ Authentication สำหรับ Web Application
- เขียนโดย Gemini
การยืนยันตัวตน (Authentication) เป็นหัวใจสำคัญของความปลอดภัยใน Web Application บทความนี้จะอธิบายและเปรียบเทียบระหว่างสองแนวทางหลักที่นิยมใช้ในปัจจุบัน คือ Session-based และ Token-based
1. Session-based Authentication
เป็นวิธีการแบบดั้งเดิม (Traditional) ที่เน้นการเก็บสถานะการล็อกอินไว้ที่ฝั่ง Server (Stateful)
กระบวนการทำงาน (Workflow)
- User Login: ผู้ใช้ส่ง Username/Password จาก Frontend ไปยัง Backend
- Create Session: เมื่อตรวจสอบข้อมูลถูกต้อง Backend จะสร้าง "Session ID" และบันทึกข้อมูลผู้ใช้ (เช่น User ID, Role) ลงในหน่วยความจำของ Server หรือ Database
- Set Cookie: Backend ส่ง Session ID กลับมาหา Frontend ผ่านทาง Set-Cookie header (มักจะตั้งค่าเป็น HttpOnly เพื่อความปลอดภัย)
- Authenticated Request: ในทุกๆ Request ถัดไป Browser จะแนบ Cookie ที่มี Session ID ไปให้ Server โดยอัตโนมัติ
- Validation: Server ตรวจสอบ Session ID จาก Cookie เทียบกับข้อมูลใน Storage หากตรงกันถือว่ายืนยันตัวตนสำเร็จ
ข้อดีและข้อเสีย
| ข้อดี | ข้อเสีย |
|---|---|
|
|
2. Token-based Authentication (JWT)
เป็นวิธีการที่นิยมใน Modern Web App และ Microservices โดยเน้นการไม่เก็บสถานะที่ Server (Stateless) แต่ใช้การเข้ารหัสข้อมูลยืนยันตัวตนส่งไปให้ Client ถือไว้แทน (นิยมใช้มาตรฐาน JSON Web Token - JWT)
กระบวนการทำงาน (Workflow)
- User Login: ผู้ใช้ส่ง Username/Password ไปยัง Backend
- Generate Token: Backend ตรวจสอบข้อมูล หากถูกต้องจะสร้าง Token (String ยาวๆ ที่ผ่านการเข้ารหัสและลงลายเซ็น Digital Signature) ซึ่งบรรจุข้อมูลผู้ใช้ (Payload) ไว้ข้างใน
- Send Token: Backend ส่ง Token กลับไปให้ Frontend (ปกติส่งเป็น JSON response)
- Store Token: Frontend ต้องเขียนโค้ดเพื่อเก็บ Token เอง (เช่น เก็บใน LocalStorage หรือ Cookie)
- Authenticated Request: Frontend ต้องแนบ Token ไปใน HTTP Header (มักใช้ header:
Authorization: Bearer <token>) - Validation: Server ตรวจสอบความถูกต้องของลายเซ็น (Signature) ใน Token โดยใช้ Secret Key หากถอดรหัสได้และ Token ยังไม่หมดอายุ ก็จะอนุญาตให้เข้าถึงข้อมูล (ไม่ต้อง Query Database เพื่อหา Session)
ข้อดีและข้อเสีย
| ข้อดี | ข้อเสีย |
|---|---|
|
|
ตารางเปรียบเทียบ (Comparison)
| หัวข้อ | Session-based | Token-based (JWT) |
|---|---|---|
| State | Stateful (เก็บที่ Server) | Stateless (เก็บที่ Client/Token) |
| Storage | Server Memory / Database | Client (LocalStorage / Cookie) |
| Scalability | ยากกว่า (ต้องจัดการ Session Store) | ง่ายมาก |
| Logout/Revoke | ง่ายดาย (ลบ Session ทิ้ง) | ยาก (ต้องรอ Expire หรือทำ Blacklist) |
| เหมาะสำหรับ | Web App ทั่วไป, CMS, องค์กรที่เน้นความปลอดภัยสูง | SPA (Single Page App), Mobile App, API Service |
คำแนะนำเพิ่มเติมในการนำไปใช้:
- Session-based: เหมาะที่สุดสำหรับระบบหลังบ้าน (Back-office), ระบบการเงิน หรือเว็บที่ไม่ซับซ้อนมาก และต้องการควบคุมความปลอดภัยในการ Log out สูงสุด
- Token-based: เหมาะที่สุดสำหรับ Frontend ที่เป็น JavaScript Framework (React, Vue, Angular) หรือกรณีที่มี Mobile Application ใช้งาน API ร่วมด้วย
Backend
เราจะเริ่มจากส่วน backend ก่อน เราจะสร้างตารางเพื่อเก็บข้อมูล user ทั้ง username, ชื่อ รวมไปถึงรหัสผ่าน แต่เราจะไม่เก็บรหัสผ่าน ไว้ตรง ๆ เราจะเก็บรหัสผ่านที่ผ่านการ hashed แล้ว การ hash ทำให้เราสามารถตรวจสอบได้ว่ารหัสผ่านที่ผู้ใช้ป้อนนั้นถูกต้อง แต่ไม่สามารถสร้างรหัสผ่านย้อนกลับได้ ถ้าระบบเรารั่วไหล ข้อมูลรหัสผ่านของ user ก็จะยังปลอดภัย
เราจะเริ่มต้นจากโค้ดเก่า ใหั activate virtual environment ถ้าเป็น linux/mac เรียก
source ./venv/bin/activate
ถ้าเป็น windows เรียก
venv\Scripts\activate.bat
และอย่าลืม export / set FLASK_APP เป็น main.py ด้วย
การ hash ด้วย Bycrypt
เมื่อ activate venv แล้ว เราจะติดตั้ง library Flask-Bcrypt สำหรับ hash รหัสผ่าน โดยสั่ง
pip install flask-bcrypt
เราจะทดลองดูการ hash กันก่อน โดยเราจะสั่งใน python command line ให้เรียก python และทดลองตามด้านล่าง (เครื่องหมาย >>> คือ prompt ของ python)
เราจะเริ่มโดย import ฟังก์ชัน generate_password_hash และ check_password_hash
>>> from flask_bcrypt import generate_password_hash, check_password_hash
เราทดลองสั่งให้สร้าง password_hash ดังนี้
>>> generate_password_hash('helloworld')
เราจะได้ค่าคืนเป็น binary แทนรหัสผ่านที่ hash แล้ว ถ้าลองสั่งหลาย ๆ ครั้ง เราจะได้ค่าที่ไม่ซ้ำกัน
>>> generate_password_hash('helloworld')
b'$2b$12$l92BQ83xZ8FsanU8dfgxoet.6qFe65T2XkRARz71ZlHgUyHqiUZN6'
>>> generate_password_hash('helloworld')
b'$2b$12$EvXcgTF.AzWnlwewuyNHeOMQ0aaMemtkTmIakgNdkPznLTvaSJtI6'
>>> generate_password_hash('helloworld')
b'$2b$12$nAun8TkUFLM9qV221Grg.ucwRG.16lL3Dr/c4LBqMTgc2wdDKdbD2'
ทั้งนี้เนื่องจากฟังก์ชันไม่ได้นำรหัสผ่านไป hash เพียงอย่างเดียว แต่มีการสุ่ม salt ที่เป็นสตริงเพิ่มเติม ใส่ไปในการ hash ด้วย เพื่อป้องกันไม่ให้การตรวจสอบรหัสผ่านโดยอาศัยตารางทำได้โดยง่าย ถ้าลองดูรายละเอียด จะพบว่าสตริงที่ได้แบ่งได้เป็นส่วน ๆ (คั่นด้วย $) ดังนี้
- อัลกอริทึม: 2b
- จำนวนรอบ: 12
- salt: l92BQ83xZ8FsanU8dfgxoe (ยาว 22 ตัวอักษร)
- รหัสผ่าน+salt ที่ hash แล้ว: t.6qFe65T2XkRARz71ZlHgUyHqiUZN6
ดังนั้นในการตรวจสอบรหัสผ่าน เราจะต้องนำรหัสผ่านที่ผู้ใช้ป้อน และ password hash มาประมวลผล โดยแกะค่า salt จาก hash ก่อน จากนั้นนำมารวมกับรหัสผ่าน ก่อนจะทำไป hash อีกที
ทดลองตรวจรหัสผ่าน
>>> pw = generate_password_hash('helloworld')
>>> check_password_hash(pw,'helloworld')
True
>>> check_password_hash(pw,'goodmorning')
False
>>> check_password_hash(pw,'hello')
False
User model
class User(db.Model):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(100), unique=True)
full_name: Mapped[str] = mapped_column(String(200))
hashed_password: Mapped[str] = mapped_column(String(100))
เพิ่ม migration
flask db migrate -m "Add User"
รัน migration
flask db upgrade
เราจะเพิ่มให้ user สามารถตรวจสอบรหัสผ่านได้ผ่านทาง bcrypt
ที่ตอนต้นไฟล์ models.py เพิ่มการ import ดังด้านล่าง
from flask_bcrypt import generate_password_hash, check_password_hash
จากนั้นเพิ่ม method เหล่านี้ลงไปใน class User
class User(db.Model):
// .. ละของเดิมไว้
def set_password(self, password):
self.hashed_password = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.hashed_password, password)
Registration CLI
@app.cli.command("create-user")
@click.argument("username")
@click.argument("full_name")
@click.argument("password")
def create_user(username, full_name, password):
print(username, full_name, password)