ผลต่างระหว่างรุ่นของ "01204223/authentication"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
แถว 312: แถว 312:
  
 
เนื่องจาก backend ของเราตอนนี้ ปิด route ที่อ่านข้อมูลด้วย jwt_required หมดแล้ว การรันส่วน react จะขึ้น error จำนวนมาก ไม่ต้องตกใจ
 
เนื่องจาก backend ของเราตอนนี้ ปิด route ที่อ่านข้อมูลด้วย jwt_required หมดแล้ว การรันส่วน react จะขึ้น error จำนวนมาก ไม่ต้องตกใจ
 +
 +
ถ้าไม่อยากเห็น error ที่ alert ขึ้นมา ให้ไป comment ส่วน alert ออก ดังตัวอย่างด้านล่าง
 +
 +
<syntaxhighlight lang="javascript">
 +
  async function fetchTodoList() {
 +
    try {
 +
      // ... ละไว้
 +
    } catch (err) {
 +
      // **** comment บรรทัด alert ด้านล่างนี้
 +
      // alert("Failed to fetch todo list from backend. Make sure the backend is running.");
 +
    }
 +
  }
 +
</syntaxhighlight>
  
 
=== ทดลอง Router ===
 
=== ทดลอง Router ===
แถว 327: แถว 340:
 
}}
 
}}
  
 +
แนวคิดของ Router คือเราจะครอบ App ทั้งหมดของเราไว้ด้วย BrowserRouter ภายในนั้นเราจะนิยาม Route ต่าง ๆ ที่จะถูกแยกไป render ตาม path ตัวอย่างโครงของการใช้งานจะเป็นดังด้านล่าง
 +
 +
<syntaxhighlight lang="html">
 +
<BrowserRouter>
 +
  <Routes>
 +
    <Route path="/" element={
 +
      สิ่งที่จะถูกแสดงเมื่อ path = /
 +
    } />
 +
 +
    <Route path="/about" element={
 +
      สิ่งที่จะถูกแสดงเมื่อ path = /about
 +
    } />
 +
 +
    <Route path="/login" element={
 +
      สิ่งที่จะถูกแสดงเมื่อ path = /login
 +
    } />
 +
 +
    <Route path="/logout" element={
 +
      สิ่งที่จะถูกแสดงเมื่อ path = /logout
 +
    } />
 +
  </Routes>
 +
</BrowserRouter>
 +
</syntaxhighlight>
 +
 +
เราจะทดลองโดยการเพิ่มหน้า about ลงไปใน todo app ของเราสักเล็กน้อย (ไม่มีประโยชน์อะไร)
 +
 +
<syntaxhighlight>
 +
function App() {
 +
  // ... ละส่วนบนไว้ ...
 +
 +
  return (
 +
    <BrowserRouter>
 +
      <Routes>
 +
        <Route
 +
          path="/"
 +
          element={
 +
            <>
 +
              <!-- ********** ย้ายทั้งก้อนเดิม มาไว้ด้านใน Route นี้ ******** --->
 +
              <h1>Todo List</h1>
 +
 
 +
 +
              <!-- ********** เพิ่มลิงก์ About ******** --->
 +
              <br/>
 +
              <a href="/about">About</a>
 +
            </>
 +
          }
 +
        />
 +
        <Route
 +
          path="/about"
 +
          element={
 +
            <>
 +
              <h1>About</h1>
 +
              <p>This is a simple todo list application built with React and Flask.</p>
 +
              <a href="/">Back to Home</a>
 +
            </>
 +
          }
 +
        />
 +
      </Routes>
 +
    </BrowserRouter>
 +
  )
 +
}
 +
</syntaxhighlight>
 
=== Login form ===
 
=== Login form ===
  

รุ่นแก้ไขเมื่อ 01:19, 20 กุมภาพันธ์ 2569

ส่วนหนึ่งของวิชา 01204223

ในโครงงานของเรา เราจะใช้การทำ authentication ด้วย Token อย่างไรก็ตาม ยังมีวิธีที่ทำได้อีกหลายวิธี ด้านล่างเป็นความรู้พื้นฐานที่อ่านประกอบได้

แนวทางการทำ Authentication สำหรับ Web Application

เขียนโดย Gemini

การยืนยันตัวตน (Authentication) เป็นหัวใจสำคัญของความปลอดภัยใน Web Application บทความนี้จะอธิบายและเปรียบเทียบระหว่างสองแนวทางหลักที่นิยมใช้ในปัจจุบัน คือ Session-based และ Token-based

1. Session-based Authentication

เป็นวิธีการแบบดั้งเดิม (Traditional) ที่เน้นการเก็บสถานะการล็อกอินไว้ที่ฝั่ง Server (Stateful)

กระบวนการทำงาน (Workflow)

  1. User Login: ผู้ใช้ส่ง Username/Password จาก Frontend ไปยัง Backend
  2. Create Session: เมื่อตรวจสอบข้อมูลถูกต้อง Backend จะสร้าง "Session ID" และบันทึกข้อมูลผู้ใช้ (เช่น User ID, Role) ลงในหน่วยความจำของ Server หรือ Database
  3. Set Cookie: Backend ส่ง Session ID กลับมาหา Frontend ผ่านทาง Set-Cookie header (มักจะตั้งค่าเป็น HttpOnly เพื่อความปลอดภัย)
  4. Authenticated Request: ในทุกๆ Request ถัดไป Browser จะแนบ Cookie ที่มี Session ID ไปให้ Server โดยอัตโนมัติ
  5. Validation: Server ตรวจสอบ Session ID จาก Cookie เทียบกับข้อมูลใน Storage หากตรงกันถือว่ายืนยันตัวตนสำเร็จ

ข้อดีและข้อเสีย

ข้อดี ข้อเสีย
  • Security: ควบคุมได้ง่าย (เช่น สั่ง Invalidate session เพื่อเตะผู้ใช้ออกได้ทันที)
  • Code Simplicity: Framework ส่วนใหญ่รองรับมาแต่กำเนิด
  • Cookie Handling: Browser จัดการการส่ง Cookie ให้เองโดยอัตโนมัติ
  • Scalability: หากมี Server หลายตัว (Load Balancing) ต้องทำ Sticky Session หรือใช้ Redis เพื่อแชร์ Session Store ร่วมกัน
  • Performance: เปลืองทรัพยากร Server ในการเก็บข้อมูล Session ของผู้ใช้ทุกคน
  • CSRF: มีความเสี่ยงต่อการโจมตีแบบ CSRF (Cross-Site Request Forgery) หากไม่ป้องกันให้ดี

2. Token-based Authentication (JWT)

เป็นวิธีการที่นิยมใน Modern Web App และ Microservices โดยเน้นการไม่เก็บสถานะที่ Server (Stateless) แต่ใช้การเข้ารหัสข้อมูลยืนยันตัวตนส่งไปให้ Client ถือไว้แทน (นิยมใช้มาตรฐาน JSON Web Token - JWT)

กระบวนการทำงาน (Workflow)

  1. User Login: ผู้ใช้ส่ง Username/Password ไปยัง Backend
  2. Generate Token: Backend ตรวจสอบข้อมูล หากถูกต้องจะสร้าง Token (String ยาวๆ ที่ผ่านการเข้ารหัสและลงลายเซ็น Digital Signature) ซึ่งบรรจุข้อมูลผู้ใช้ (Payload) ไว้ข้างใน
  3. Send Token: Backend ส่ง Token กลับไปให้ Frontend (ปกติส่งเป็น JSON response)
  4. Store Token: Frontend ต้องเขียนโค้ดเพื่อเก็บ Token เอง (เช่น เก็บใน LocalStorage หรือ Cookie)
  5. Authenticated Request: Frontend ต้องแนบ Token ไปใน HTTP Header (มักใช้ header: Authorization: Bearer <token>)
  6. Validation: Server ตรวจสอบความถูกต้องของลายเซ็น (Signature) ใน Token โดยใช้ Secret Key หากถอดรหัสได้และ Token ยังไม่หมดอายุ ก็จะอนุญาตให้เข้าถึงข้อมูล (ไม่ต้อง Query Database เพื่อหา Session)

ข้อดีและข้อเสีย

ข้อดี ข้อเสีย
  • Stateless & Scalable: Server ไม่ต้องจำอะไร ขยาย Server (Scale out) ได้ง่ายมาก
  • Cross-Domain: สามารถใช้ Token เดียวกันกับหลายๆ Domain หรือ Mobile App ได้ง่าย
  • Performance: ลดภาระการอ่านเขียน Database/Memory เพื่อตรวจสอบ Session
  • Revocation Difficulties: การยกเลิก Token ก่อนหมดอายุทำได้ยาก (เพราะ Server ไม่ได้เก็บสถานะ) ต้องใช้เทคนิค Blacklist หรือลดอายุ Token ให้สั้นลง (Short-lived)
  • Storage Security: หากเก็บใน LocalStorage อาจเสี่ยงต่อ XSS
  • Payload Size: Token มีขนาดใหญ่กว่า Session ID อาจเปลือง Bandwidth หากข้อมูลเยอะ

ตารางเปรียบเทียบ (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

เราจะสร้างตารางเพื่อเก็บข้อมูลของผู้ใช้ ให้เพิ่มโมเดลลงในไฟล์ models.py ดังด้านล่าง เราจะเก็บ username, full_name และ hashed_password (เราจะไม่เก็บรหัสผ่านโดยตรง)

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

ที่เราเพิ่มไปจะเป็นโมเดลเปล่า ๆ อย่างไรก็ตาม ปกติเรามักจะนำ function ที่เกี่ยวข้องกับโมเดลไปเขียนไว้ให้เป็น method ของโมเดลเลย ในที่นี้ ส่วนของการตั้งและตรวจสอบรหัสผ่านเป็นกิจกรรมที่ใกล้ชิดกับโมเดล User มาก ๆ ดังนั้น เราจะเพิ่ม method ที่เกี่ยวข้องลงไปในโมเดล 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)


@app.cli.command("create-user")
@click.argument("username")
@click.argument("full_name")
@click.argument("password")
def create_user(username, full_name, password):
    user = User.query.filter_by(username=username).first()
    if user:
        click.echo("User already exists.")
        return
    user = User(username=username, full_name=full_name)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()
    click.echo(f"User {username} created successfully.")

API

เราจะเริ่มโดยการตรวจสอบรหัสผ่าน

@app.route('/api/login/', methods=['POST'])
def login():
    data = request.get_json()
    if not data or 'username' not in data or 'password' not in data:
        return jsonify({'error': 'Username and password are required'}), 400

    user = User.query.filter_by(username=data['username']).first()
    if not user or not user.check_password(data['password']):
        return jsonify({'error': 'Invalid username or password'}), 401

    return jsonify({'message': 'Login successful'})

Token

เราจะใช้ไลบรารี Flask-JWT-Extended ในการสร้างและตรวจสอบ JSON Web Token (JWT) โดยติดตั้งดังนี้

pip install flask-jwt-extended
from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required
from flask_jwt_extended import JWTManager
app.config['JWT_SECRET_KEY'] = 'fdsjkfjioi2rjshr2345hrsh043j5oij5545'
jwt = JWTManager(app)

login

@app.route('/api/login/', methods=['POST'])
def login():
    data = request.get_json()
    if not data or 'username' not in data or 'password' not in data:
        return jsonify({'error': 'Username and password are required'}), 400

    user = User.query.filter_by(username=data['username']).first()
    if not user or not user.check_password(data['password']):
        return jsonify({'error': 'Invalid username or password'}), 401

    access_token = create_access_token(identity=user.username)
    return jsonify(access_token=access_token)

อะไรอยู่ใน token

การป้องกัน route ใน Flask

@app.route('/api/todos/', methods=['GET'])
@jwt_required()
def get_todos():
    todos = TodoItem.query.all()
    return jsonify([todo.to_dict() for todo in todos])

ในที่นี้เรายังไม่ได้นำข้อมูลอะไรของผู้ใช้ไปทำอะไรเราเลยไม่ได้มีการแกะข้อมูลมาจาก JWT อย่างไรก็ตาม ถ้าเราต้องการข้อมูล เราจะใช้ฟังก์ชัน get_jwt_identity

จัดการรายการ library และ commit

เราได้ติดตั้งไลบรารีไปหลายตัว เราจะต้องอัพเกรดไฟล์ requirements.txt (ใน backend) เพื่อเก็บรายการไลบรารีเหล่านี้ การจัดเก็บดังกล่าวจะทำให้เราสามารถนำโค้ดไปรันที่อื่นได้ง่าย (รวมถึงการทำ github action เพื่อทดสอบโค้ดส่วน backend ด้วย)

เนื่องจากเราติดตั้งไลบรารีด้วย pip เราสามารถสั่ง

pip freeze

ให้ pip แสดงรายการไลบรารีได้ (ให้ทดลองสั่งดู ให้กดสั่งตอนอยู่ใน virtual environment แล้ว) ดังตัวอย่างด้านล่าง (อาจจะไม่เหมือนกันทั้งหมด)

aptdaemon==2.0.2
asn1crypto==1.5.1
attrs==25.3.0
autocommand==2.2.2
babel==2.17.0
bcc==0.31.0
bcrypt==4.2.0
beautifulsoup4==4.13.4
blinker==1.9.0
.... และอื่น ๆ ....

รายการดังกล่าวจะมีลักษณะเดียวกับรายการใน requirements.txt ดังนั้นเราจะสามารถสั่ง

pip freeze > requirements.txt

เพื่ออัพเดท requirements.txt ได้เลย

เมื่อจัดการเรียบร้อยแล้ว เราจะเก็บงานที่เราทำลงใน git ต่อ

ให้ add file ที่เราแก้ไข (อย่าลืม migrations ใน backend/migrations/versions) จากนั้น commit งานให้เรียบร้อย ให้ใช้ git message ที่มีความหมาย

🄶 commit งานที่ทำ แล้ว push กลับไปที่ github ด้วย

Frontend

เนื่องจาก backend ของเราตอนนี้ ปิด route ที่อ่านข้อมูลด้วย jwt_required หมดแล้ว การรันส่วน react จะขึ้น error จำนวนมาก ไม่ต้องตกใจ

ถ้าไม่อยากเห็น error ที่ alert ขึ้นมา ให้ไป comment ส่วน alert ออก ดังตัวอย่างด้านล่าง

  async function fetchTodoList() {
    try {
      // ... ละไว้
    } catch (err) {
      // **** comment บรรทัด alert ด้านล่างนี้
      // alert("Failed to fetch todo list from backend. Make sure the backend is running.");
    }
  }

ทดลอง Router

ในส่วนของ React UI, หน้าจอเราจะมีความซับซ้อนขึ้น (หลัก ๆ คือจะมีเหมือนหลายหน้า) เราจะเริ่มต้องการเครื่องมือที่ทำให้เราจัดการหน้าจอได้เหมือนการกด link และ redirection ใน html     เครื่องมือมาตรฐานคือ React Router

เราจะติดตั้งโดยสั่ง

npm install react-router-dom

เราจะไปทดลองแนวคิดของ router กันก่อน

คุณอาจจะเห็น message เกี่ยวกับ vulnerabilities จะมีรายละเอียดวิธีการจัดการให้ต่อไป

แนวคิดของ Router คือเราจะครอบ App ทั้งหมดของเราไว้ด้วย BrowserRouter ภายในนั้นเราจะนิยาม Route ต่าง ๆ ที่จะถูกแยกไป render ตาม path ตัวอย่างโครงของการใช้งานจะเป็นดังด้านล่าง

<BrowserRouter>
  <Routes>
    <Route path="/" element={
      ิ่งที่จะถกแสดงเมื่ path = /
    } />

    <Route path="/about" element={
      ิ่งที่จะถกแสดงเมื่ path = /about
    } />

    <Route path="/login" element={
      ิ่งที่จะถกแสดงเมื่ path = /login
    } />

    <Route path="/logout" element={
      ิ่งที่จะถกแสดงเมื่ path = /logout
    } />
  </Routes>
</BrowserRouter>

เราจะทดลองโดยการเพิ่มหน้า about ลงไปใน todo app ของเราสักเล็กน้อย (ไม่มีประโยชน์อะไร)

function App() {
  // ... ละส่วนบนไว้ ...

  return (
    <BrowserRouter>
      <Routes>
        <Route 
          path="/" 
          element={
            <>
              <!-- ********** ย้ายทั้งก้อนเดิม มาไว้ด้านใน Route นี้ ******** --->
              <h1>Todo List</h1>
   

              <!-- ********** เพิ่มลิงก์ About ******** --->
              <br/>
              <a href="/about">About</a>
            </>
          } 
        />
        <Route 
          path="/about" 
          element={
            <>
              <h1>About</h1>
              <p>This is a simple todo list application built with React and Flask.</p>
              <a href="/">Back to Home</a>
            </>
          } 
        />
      </Routes>
    </BrowserRouter>
  )
}

Login form

Context

Protected routes