ผลต่างระหว่างรุ่นของ "01204223/authentication"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
| แถว 360: | แถว 360: | ||
ดังนั้นในขั้นแรกเราจะย้ายทุกอย่างที่เป็นการทำงานเกี่ยวกับ TodoList ออกจาก App ไปสร้างเป็น Component ใหม่ อย่างไรก็ตาม เราจะพยายามเก็บพวก config เช่น url ต่าง ๆ ไว้ใน App | ดังนั้นในขั้นแรกเราจะย้ายทุกอย่างที่เป็นการทำงานเกี่ยวกับ TodoList ออกจาก App ไปสร้างเป็น Component ใหม่ อย่างไรก็ตาม เราจะพยายามเก็บพวก config เช่น url ต่าง ๆ ไว้ใน App | ||
| + | เราจะย้ายโค้ดเดิมจาก App.jsx ไปสร้างเป็น TodoList.jsx ดังตัวอย่างด้านล่าง | ||
| + | หลัก ๆ ที่เราต้องแก้คือเปลี่ยนชื่อฟังก์ชันจาก App เป็น TodoList และใส่ prop apiUrl | ||
| + | |||
| + | <syntaxhighlight lang="javascript"> | ||
| + | import { useState, useEffect } from 'react' | ||
| + | import './App.css' | ||
| + | |||
| + | import TodoItem from './TodoItem.jsx' | ||
| + | |||
| + | function TodoList({apiUrl}) { | ||
| + | const TODOLIST_API_URL = apiUrl; | ||
| + | |||
| + | const [todoList, setTodoList] = useState([]); | ||
| + | const [newTitle, setNewTitle] = useState(""); | ||
| + | |||
| + | // ... โค้ดเดิม | ||
| + | } | ||
| + | |||
| + | export default TodoList; // อย่าลืมแก้ export | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | จากนั้นเราจะทำความสะอาด App.jsx ของเรา โดยนำส่วนที่ย้ายออกไปแล้วทิ้งไป | ||
| + | |||
| + | <syntaxhighlight lang="javascript"> | ||
| + | import { BrowserRouter, Routes, Route } from "react-router-dom"; | ||
| + | import { AuthProvider } from './context/AuthContext.jsx'; | ||
| + | import './App.css' | ||
| + | |||
| + | import TodoList from './TodoList.jsx' | ||
| + | |||
| + | function App() { | ||
| + | const TODOLIST_API_URL = 'http://localhost:5000/api/todos/'; | ||
| + | |||
| + | return ( | ||
| + | <TodoList apiUrl={TODOLIST_API_URL}/> | ||
| + | ) | ||
| + | } | ||
| + | |||
| + | export default App | ||
| + | </syntaxhighlight> | ||
=== ทดลอง Router === | === ทดลอง Router === | ||
รุ่นแก้ไขเมื่อ 03:00, 20 กุมภาพันธ์ 2569
- ส่วนหนึ่งของวิชา 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
เราจะสร้างตารางเพื่อเก็บข้อมูลของผู้ใช้ ให้เพิ่มโมเดลลงในไฟล์ 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
เพื่อความสะดวก เราจะยังไม่ทำหน้าลงทะเบียนผ่านทาง react ui แต่เราจะเขียนคำสั่งใน Flask สำหรับสร้าง user ไว้ก่อน เราจะได้มี user ไว้ทดสอบได้
เราสามารถเพิ่มคำสั่งให้กับ flask ได้ คำสั่งเหล่านี้เมื่อเพิ่มแล้ว เราสามารถเรียกผ่าน flask ได้เลย ตัวอย่างเช่น เดี๋ยวเราจะเพิ่มคำสั่ง create-user ซึ่งจะทำให้เราสั่ง
flask create-user admin "Admin Fullname" helloworld
เพื่อสร้าง user ที่มี username=admin, full_name="Admin Fullname" และ password=helloworld ได้
Flask ใช้ไลบรารีชื่อ click ในการจัดการกับ cli ดังนั้นเราจะต้องไป import มาก่อน ให้เพิ่มบรรทัดข้างล่างที่ตอนต้น main.py
import click
จากนั้นเราจะไปเพิ่มฟังก์ชันสำหรับทำงานตามคำสั่งดังกล่าวใน main.py (ควรไว้ตอนท้ายเลย) แต่เราจะครอบไว้ด้วย @app.cli.command และ @click
@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)
การระบุดังกล่าว บอก flask ว่าจะมีคำสั่งใหม่เพิ่มขึ้น รวมทั้งระบุ argument ด้วย เมื่อแก้แล้ว ทดลองเรียกคำสั่ง create-user ในรูปแบบต่าง ๆ ดู ก่อนจะแก้ต่อไป
เราจะเพิ่มโค้ดในการสร้าง user รวมทั้งตรวจสอบเพื่อป้องกันการสร้าง user ซ้ำลงในฟังก์ชัน create_user ดังด้านล่าง
@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.")
ให้ทดลองสร้าง user สัก 1 - 2 คน ให้ทดลองสั่งสร้าง username ซ้ำดูเพื่อตรวจสอบความถูกต้อง
ในการทดลองต่อไป เราจะต้อง login user คนที่เพิ่งสร้างมา ดังนั้นอย่าลืมจำรหัสผ่านไว้ด้วย ถ้าจำไม่ได้สามารถกลับมาสร้าง user เพื่อทดลองใหม่ได้
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 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
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.");
}
}
อย่างไรก็ตาม ถ้ารันเทส
npm test
ทุกอย่างจะยังสามารถทำงานได้ตามปกติ ลองหาคำตอบเองดูว่าทำไมการทำ authentication ที่เราเขียนจึงไม่มีผลกับ unit test
แยก TodoList
ก่อนที่เราจะเริ่มทำอะไรซับซ้อน เราจะแยกส่วน TodoList ทั้งหมดออกมาจาก App เพราะว่าในตอนสุดท้ายเราจะนำส่วน authentication ไปครอบทุกอย่างไว้ แต่ถ้างานของเราอยู่ใน App เราจะไปหาอะไรมาครอบ App อีกจะทำไม่ได้
ดังนั้นในขั้นแรกเราจะย้ายทุกอย่างที่เป็นการทำงานเกี่ยวกับ TodoList ออกจาก App ไปสร้างเป็น Component ใหม่ อย่างไรก็ตาม เราจะพยายามเก็บพวก config เช่น url ต่าง ๆ ไว้ใน App
เราจะย้ายโค้ดเดิมจาก App.jsx ไปสร้างเป็น TodoList.jsx ดังตัวอย่างด้านล่าง
หลัก ๆ ที่เราต้องแก้คือเปลี่ยนชื่อฟังก์ชันจาก App เป็น TodoList และใส่ prop apiUrl
import { useState, useEffect } from 'react'
import './App.css'
import TodoItem from './TodoItem.jsx'
function TodoList({apiUrl}) {
const TODOLIST_API_URL = apiUrl;
const [todoList, setTodoList] = useState([]);
const [newTitle, setNewTitle] = useState("");
// ... โค้ดเดิม
}
export default TodoList; // อย่าลืมแก้ export
จากนั้นเราจะทำความสะอาด App.jsx ของเรา โดยนำส่วนที่ย้ายออกไปแล้วทิ้งไป
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from './context/AuthContext.jsx';
import './App.css'
import TodoList from './TodoList.jsx'
function App() {
const TODOLIST_API_URL = 'http://localhost:5000/api/todos/';
return (
<TodoList apiUrl={TODOLIST_API_URL}/>
)
}
export default App
ทดลอง 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={
<>
<!-- ********** TodoList เดิม มาไว้ด้านใน Route นี้ ******** --->
<TodoList apiUrl={TODOLIST_API_URL}/>
<!-- ********** เพิ่มลิงก์ 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>
)
}เราจะสามารถลองกดไปมาระหว่างหน้าหลัก กับ About ได้
จากโครงของ Router แบบนี้ จะทำให้เราสามารถสร้างส่วนของ Route ที่มีการป้องกันด้วย authentication ได้ไม่ยากนัก
Login form
import { useState } from 'react'
import './App.css'
function LoginForm({loginUrl}) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
async function handleLogin(e) {
e.preventDefault();
}
return (
<form onSubmit={(e) => {handleLogin(e)}}>
Username:
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
<br/>
Password:
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<br/>
<button type="submit">Login</button>
</form>
);
}
export default LoginForm;
import LoginForm from './LoginForm.jsx';
function App() {
// ...
const TODOLIST_LOGIN_URL = 'http://localhost:5000/api/login/';
// ...
}
<a href="/about">About</a>
|
<a href="/login">Login</a>
<Route
path="/login"
element={
<LoginForm loginUrl={TODOLIST_LOGIN_URL} />
}
/>
async function handleLogin(e) {
e.preventDefault();
try {
const response = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: username, password: password }),
});
if (response.ok) {
const data = await response.json();
console.log(data);
alert("Login successful. access token = " + data.access_token);
} else if (response.status === 401) {
alert("Login failed: Invalid username or password");
}
} catch (error) {
console.log("Error logging in:", error);
}
}
เราอาจจะไม่ชอบเห็น alert เยอะๆ ยิ่งโดยเฉพาะตอนที่เราพิมพ์รหัสผ่านผิด
const [errorMessage, setErrorMessage] = useState("");
if (response.ok) {
//...
} else if (response.status === 401) {
setErrorMessage("Invalid username or password");
}
<form onSubmit={(e) => {login(e)}}>
{errorMessage && <p>{errorMessage}</p>}
...
</form>
เมื่อเราได้ access token มาแล้ว เราจะต้องหาวิธีส่ง token ดังกล่าวให้กับส่วนอื่น ๆ ของ App ซึ่งถ้าเราใช้การส่งค่าผ่านทาง props มันจะเป็นการส่งที่กระจายไปทั่วโค้ดมาก ๆ ดังนั้นใน React จึงมีแนวคิดที่เรียกว่า Context ที่เราสามารถใช้เพื่อส่งข้อมูลที่มีลักษณะเป็นแบบ global ได้
เราจะใช้ context ในการเก็บข้อมูลเกี่ยวกับ access token (และอาจจะรวมไปถึงข้อมูลอื่น ๆ ที่เกี่ยวข้องกับผู้ใช้) จากนั้นเราจะจัดการทำส่วน Router ให้ซ่อน Route ที่เข้าถึงไม่ได้โดยอัตโนมัติด้วยข้อมูลใน context นี้