รู้จักรูปแบบ Authentication ระหว่าง Frontend และ Backend
[ สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
เนื้อหานี้ทำมาเนื่องจากใน Web development 101 ที่ผมทำไปก่อนหน ้านี้ ผมยังไม่ได้นำเสนอวิธีการทำ Authenticaion ผ่าน Backend เลย
Session นี้เลยจะพามาทำ login และการทำเรื่องกั้นสิทธิ์ว่าสามารถทำอย่างไรได้บ้าง
เราจะแบ่งหัวข้อออกจากกันตามนี้
เราจะมาตอบทั้ง 4 คำถามนี้ใน Session นี้กัน
- เราจะเก็บ password ไว้ในฐานข้อมูลอย่างไร ?
- เราจะยืนยันได้อย่างไรว่า เราที่เข้ามาคือใคร ?
- เราจะทำ login ได้ยังไง ? และทำ login แล้วได้อะไรออกมาเป็นเครื่องยืนยันตัวตน ?
- เราจะป้องกันไม่ให้คนทั่วไปเข้ามายิง API ที่เราไม่อยากให้คนทั่วไปยิงได้ยังไง ?
z
- สำหรับ project นี้เราจะ setup docker 2 ตัวคือ mysql และ phpmyadmin เท่านั้น
- โดยโจทย์ของ mysql คือเราจะสร้าง table ชื่อ users ขึ้นมาและเราจะทำการเก็บข้อมูล email, password เอาไว่้เพื่อทำการ login
docker-compose.yml
version: "3.7"
services:
db:
container_name: mysql_db
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: tutorial
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- my_network
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
container_name: phpmyadmin
environment:
PMA_HOST: db
PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: root
ports:
- "8080:80"
depends_on:
- db
networks:
- my_network
networks:
my_network:
driver: bridge
volumes:
mysql_data:
driver: local
Structure project จะเป็นตามนี้
├── docker-compose.yml
├── index.js -- ไฟล์หลักที่เราจะทำกัน
├── package.json
└── src
└── index.html
library ที่ใช้ในรอบนี้
expressสำหรับ library node สำหรับทำ Rest APIcorsสำหรับการเปิดให้ฝั่ง Frontend สามารถยิงเข้ามาผ่าน cross domain ได้mysql2สำหรับจัดการฐานข้อมูล mysqljsonwebtokenสำหรับการเข้ารหัสข้อมูลสำหรับแนบเข้า token ตอน login สำเร็จcookie-parserสำหรับเรียกใช้และ save cookiebcryptสำหรับเข้ารหัส passwordexpress-sessionสำหรับการ login ในเคสที่ใช้ session
โดย index.js เราจะเพิ่ม code ไว้ตามนี้เพื่อ config เริ่มต้นก่อน
const cors = require("cors");
const express = require("express");
const mysql = require("mysql2/promise");
const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const bcrypt = require("bcrypt");
const app = express();
app.use(express.json());
app.use(
cors({
credentials: true,
origin: ["http://localhost:8888"],
}),
);
app.use(cookieParser());
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
}),
);
const port = 8000;
const secret = "mysecret";
let conn = null;
// function init connection mysql
const initMySQL = async () => {
conn = await mysql.createConnection({
host: "localhost",
user: "root",
password: "root",
database: "tutorial",
});
};
/* เราจะแก้ไข code ที่อยู่ตรงกลาง */
// Listen
app.listen(port, async () => {
await initMySQL();
console.log("Server started at port 8000");
});
โจทย์ของหัวข้อนี้เราจะทำ API อะไรกันบ้าง
เราจะทำ API
- POST
/api/registerสำหรับสมัครสมาชิก email, password เข้ามา (จะใช้ในข้อ 1) - POST
/api/loginสำหรับ login สมาชิกด้วย email, password เพื่อยืนยันตัวตนและเข้าสู่ระบบ (จะใช้ในข้อ 2,3) - GET
/api/usersสำหรับ list users ทั้งหมดในระบบโดยจะอนุญาตเฉพาะคนที่เป็น role admin เท่านั้น (จะใช้ในข้อที่ 4)
1. เราจะเก็บ password ไว้ในฐานข้อมูลอย่างไร ?
- ใช้ bcrypt ซึ่งเป็น password hashing function ที่สร้างขึ้นจากพื้นฐานของ Blowfish cipher เป็นการเข้ารหัสแบบทางเดียว
- ปกติการเข้ารหัสมี 2 แบบ (แบ่งแบบง่ายๆนะ) คือ เข้ารหัสแบบ decrypt ได้ (แกะข้อมูลเข้ารหัสออกมาได้) และ เข้ารหัสแบบทางเดียว (เช่น hashing function)
app.post("/api/register", async (req, res) => {
const { email, password } = req.body;
const [rows] = await conn.query("SELECT * FROM users WHERE email = ?", email);
if (rows.length) {
return res.status(400).send({ message: "Email is already registered" });
}
// Hash the password
const hash = await bcrypt.hash(password, 10);
// 10 = salt (การสุ่มค่าเพื่อเพิ่มความซับซ้อนในการเข้ารหัส)
// และมันจะถูกนำมาใช้ตอน compare
// Store the user data
const userData = { email, password: hash };
try {
const result = await conn.query("INSERT INTO users SET ?", userData);
} catch (error) {
console.error(error);
res.status(400).json({
message: "insert fail",
error,
});
}
res.status(201).send({ message: "User registered successfully" });
});
2. เราจะยืนยันได้อย่างไรว่า เราที่เข้ามาคือใคร ?
app.post("/api/login", async (req, res) => {
const { email, password } = req.body;
const [result] = await conn.query("SELECT * from users WHERE email = ?", email);
const user = result[0];
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(400).send({ message: "Invalid email or password" });
}
res.send({ message: "Login successful" });
});
3. เราจะทำ login ได้ยังไง ? และทำ login แล้วได้อะไรออกมาเป็นเครื่องยืนยันตัวตน ?
1. gen token ส่งให้ Frontend เก็บไว้
- JWT Token คือ token เข้ารหัสมาตรฐาน RFC 7519 ที่สามารถนำมา decode ข้อมูลกลับได้
- มีความปลอดภัยในแง่การส่งข้อมูลไปมาระหว่าง Frontend, Backend แต่ ไม่เหมาะสำหรับเก็บข้อมูลที่มีความปลอดภัยสูง
app.post("/api/login", async (req, res) => {
// code จากหัวข้อ 2 เรื่องเช็ค
if (!match) {
return res.status(400).send({ message: "Invalid email or password" });
}
const token = jwt.sign({ email, role: "admin" }, secret, { expiresIn: "1h" });
res.send({ message: "Login successful", token });
});
2. ใส่ผ่าน cookie เข้า domain Frontend
app.post("/api/login", async (req, res) => {
// code จากหัวข้อ 2 เรื่องเช็ค
if (!match) {
return res.status(400).send({ message: "Invalid email or password" });
}
const token = jwt.sign({ email, role: "admin" }, secret, { expiresIn: "1h" });
res.cookie("token", token, {
maxAge: 300000,
secure: true,
httpOnly: true,
sameSite: "none",
});
res.send({ message: "Login successful" });
});