ในบทความนี้ เราจะสร้างบริการ Rest API ที่ผ่านการรับรองความถูกต้องขั้นต่ำแต่ใช้งานได้อย่างสมบูรณ์ โดยใช้ประโยชน์จากเส้นทาง Next.js API และ Upstash Redis ซึ่งเราจะใช้เป็นระบบจัดเก็บข้อมูล/แคชที่เร็วสุด ๆ สำหรับข้อมูลของเรา สำหรับการตรวจสอบสิทธิ์ผู้ใช้ และสำหรับการจัดการ JWT ของเรา โปรดทราบว่าโปรเจ็กต์นี้จะไม่มีส่วนหน้า แต่จะแสดงเฉพาะ API ที่สามารถสอบถามกับไคลเอนต์ที่หลากหลายได้
ข้อกำหนดเบื้องต้น
หากต้องการติดตามบทช่วยสอน คุณจะต้องมี:
- บัญชี Upstash — ลงทะเบียนเพื่อรับบัญชีฟรีที่นี่
- ความรู้พื้นฐานของ Redis
- ความรู้พื้นฐานเกี่ยวกับเส้นทาง Next.js API
- ความรู้พื้นฐานเกี่ยวกับขั้นตอนการตรวจสอบสิทธิ์และการอนุญาต
- เครื่องมือที่คุณเลือกสำหรับส่งคำขอ HTTP
Upstash Redis คืออะไร
Upstash คือ ฐานข้อมูลระบบคลาวด์ในหน่วยความจำแบบ Serverless ขึ้นอยู่กับเรดิส เราจะใช้เพื่อจัดเก็บข้อมูลที่จะให้บริการโดย API ของเรา นอกจากนี้เรายังจะเก็บไว้ใน Upstash Redis ฐานผู้ใช้และโทเค็นผู้ใช้ของเรา
สิ่งที่เราจะสร้าง
เราจะเขียนโค้ดบริการ REST API ที่จะอนุญาตให้แอปพลิเคชันไคลเอ็นต์สามารถขอข้อมูลจากบริการนั้นได้ (ในกรณีนี้คือรายชื่อภาพยนตร์) เราจะรักษาความปลอดภัยอุปกรณ์ปลายทางโดยใช้ JWT เราจะเขียนโค้ดบริการเข้าสู่ระบบ API เพื่อรับโทเค็น และเรายังใช้เวิร์กโฟลว์โทเค็นการรีเฟรชด้วย
เราจะไม่เน้น เกี่ยวกับการพัฒนาลูกค้า (เนื่องจากเรากำลังสร้างบริการที่ 'ไม่มีความเห็น') แต่เราจะจัดเตรียมข้อกำหนดของบริการของเราเพื่อให้ทุกคนสามารถสร้างลูกค้าได้
พื้นที่เก็บข้อมูลและการสาธิต
หากต้องการติดตามคุณอาจต้องการโคลนพื้นที่เก็บข้อมูลโปรเจ็กต์
แหล่งที่มาบน GitHub
คุณยังสามารถลองสาธิตได้ที่ URL ต่อไปนี้:
https://upstash-dwov9jbiq-popland.vercel.app/api/auth/signin
หากต้องการเชื่อมต่อกับบริการ ให้ส่งคำขอ POST HTTP ผ่านชื่อผู้ใช้ (me@home.org ) และรหัสผ่าน (รหัสผ่าน ) เหมือนตัวอย่างต่อไปนี้ (ใช้บุรุษไปรษณีย์):
การตั้งค่าฐานข้อมูล Redis
ก่อนอื่น คุณต้องลงทะเบียนกับ Upstash Redis (แผนฟรีจะใช้ได้สำหรับวัตถุประสงค์ในการทดสอบ) เมื่อเข้าสู่ระบบในคอนโซลเสร็จแล้ว คุณจะสามารถสร้างฐานข้อมูลใหม่ได้:
ดำเนินการต่อโดยคลิก “สร้างฐานข้อมูล” ตั้งชื่อเป็น MovieManager และตั้งเป็น Global ตอนนี้เราเพิ่มข้อมูลจำลองโดยใช้ Upstash CLI
เราจะเพิ่มภาพยนตร์บางเรื่องเป็นแฮช Redis (โดยพื้นฐานแล้วเป็นวัตถุ) โดยใช้คำสั่ง HMSET:
hmset movie:’Dr. Strangelove’ director ‘Stanley Kubrick’ year 1964
hmset movie:’2001: A Space Odyssey’ director ‘Stanley Kubrick’ year 1968
hmset movie:’Pulp Fiction’ director ‘Quentin Tarantino’ year 1994
hmset movie:’Django Unchained’ director ‘Quentin Tarantino’ year 2012
นอกจากนี้เรายังจะเพิ่มผู้ใช้ที่จะได้รับอนุญาตให้เข้าถึงข้อมูล ผู้ใช้จะเป็นแฮช Redis:
hmset user:’me@home.org’ password $2b$10$zctxUVDyy3jzvSp68oKpMOnkyra4R.NzOFVh9aii3Y43X7XtetoyK level 0
โปรดทราบ :รหัสผ่านถูกเข้ารหัสด้วย 00 (โดยทั่วไปคือ รหัสผ่าน ) โดยปกติแล้ว ผู้ใช้ที่ต้องการเข้าถึงการลงทะเบียน API ผ่านทางเว็บไซต์ (หรือรับข้อมูลรับรองบนเว็บไซต์) ในตัวอย่างนี้ เราไม่ได้จัดเตรียมจุดสิ้นสุดการลงทะเบียน
ทุกคำสั่งที่ป้อนใน Upstash CLI ควรให้การตอบกลับที่ตกลง หากทุกอย่างถูกต้องและคุณไปที่ เบราว์เซอร์ข้อมูล และเลือกแฮช คุณจะมีรายการข้อมูลที่คุณใส่ไว้
ขั้นตอนการอนุญาต
ตามที่กล่าวไว้ก่อนหน้านี้ ตำแหน่งข้อมูลของเราไม่สามารถเข้าถึงได้แบบสาธารณะ ดังนั้นเราจึงต้องมีวิธีการตรวจสอบสิทธิ์และให้สิทธิ์ผู้ใช้ สำหรับการรับรองความถูกต้อง เราจัดให้มีจุดสิ้นสุดการเข้าสู่ระบบ สำหรับการอนุญาต จุดสิ้นสุดที่ได้รับการป้องกันจำเป็นต้องมีส่วนหัวการอนุญาตเพื่อส่งไปพร้อมกับคำขอ นี่คือวิธีการทำงานของเวิร์กโฟลว์โดยละเอียด:
- ผู้ใช้ร้องขอปลายทางการลงชื่อเข้าใช้ โดยโพสต์ชื่อผู้ใช้และรหัสผ่าน
- เซิร์ฟเวอร์พยายามตรวจสอบสิทธิ์ผู้ใช้ หากผู้ใช้ถูกต้อง เซิร์ฟเวอร์สร้างและส่ง JWT (JSON Web Token) และโทเค็นการรีเฟรชกลับมา โทเค็นการรีเฟรชจะถูกจัดเก็บไว้ในอินสแตนซ์ Upstash Redis ของเราด้วย
- ลูกค้าได้รับโทเค็นกลับมาและเก็บไว้ที่ไหนสักแห่ง (เป็นความรับผิดชอบของลูกค้าว่าจะจัดเก็บอย่างไร/ที่ไหน)
- ไคลเอนต์ร้องขอจุดสิ้นสุดที่ได้รับการป้องกัน โดยส่ง JWT ในส่วนหัว
- เซิร์ฟเวอร์ได้รับ JWT ตรวจสอบ และหากได้รับการยืนยันแล้ว ก็จะส่งข้อมูลที่ลูกค้าร้องขอกลับมา
- เมื่อ JWT หมดอายุหรือใกล้จะหมดอายุ ลูกค้าสามารถขอ JWT ใหม่โดยไม่ต้องเข้าสู่ระบบใหม่ โดยส่ง Refresh Token ไปยังจุดสิ้นสุดที่ระบุ
- เซิร์ฟเวอร์ได้รับ Refresh Token ตรวจสอบ และหากการตรวจสอบเป็นบวก จะต้องออก JWT และ Refresh Token ใหม่ ให้ส่งกลับไปยังไคลเอนต์ และเก็บ Refresh Token ใหม่อีกครั้ง
JWT และ Refresh Token อยู่ในรูปแบบเดียวกัน ได้ข้อมูลเกือบเหมือนกันแต่ใช้ 2 คีย์ที่แตกต่างกัน (เราจะตั้งค่าใน 13 ของเรา ) และมีการหมดอายุที่แตกต่างกันสองแบบ:แบบสั้นสำหรับ JWT (เนื่องจากเป็นโทเค็นที่ใช้มากที่สุดในระหว่างเซสชัน เราจะทำให้หมดอายุเร็วๆ นี้ ในกรณีที่ถูกดักฟัง) และแบบยาวสำหรับ Refresh Token ระยะเวลาของทั้งสองอย่างขึ้นอยู่กับความปลอดภัยที่คุณต้องอยู่ โดยปกติ JWT จะหมดอายุภายในไม่ถึงหนึ่งชั่วโมง และ Refresh Token จะอยู่ได้หนึ่งเดือน หากโทเค็นทั้งสองหมดอายุ ผู้ใช้จะต้องเข้าสู่ระบบอีกครั้ง
การตั้งค่าโครงการ
เมื่อเราเสร็จสิ้นกับฐานข้อมูล Upstash Redis แล้ว เราก็สามารถเริ่มต้นโครงการของเราได้ ก่อนอื่น เราสร้างโปรเจ็กต์ Next.js ใหม่:
npx create-next-app upstash-jwt
จากนั้นเราเข้าไปในโฟลเดอร์ที่สร้างขึ้นใหม่ 29 และเราติดตั้งโมดูลที่จำเป็น:
npm i bcrypt jsonwebtoken @upstash/redis
สร้าง 33 ไฟล์เพื่อจัดเก็บคีย์ของคุณและกรอกข้อมูลที่ถูกต้อง
SECRET_TOKEN=
SECRET_RTOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= สร้าง SECRET_TOKEN และ SECRET_RTOKEN ที่จะใช้ในการสร้าง JWT โปรดจำไว้ว่าคีย์เหล่านี้ควรถูกเก็บเป็นความลับและควรเป็นแบบสุ่ม/คาดเดาได้ยาก คุณสามารถใช้สตริง Hex แบบ 64 บิตได้ รับ UPSTASH_REDIS_REST_URL และ UPSTASH_REDIS_REST_TOKEN จากคอนโซล Upstash แท็บรายละเอียด ส่วน Rest API:
ตอนนี้เราสามารถเริ่มวางโครงร่างจุดสิ้นสุดของเราได้แล้ว:
POST /auth/signin มันเข้าสู่ระบบผู้ใช้ โดยต้องส่งอีเมลและรหัสผ่านเป็นวัตถุ JSON 40 โดยจะส่งคืนออบเจ็กต์ JSON พร้อมข้อมูลผู้ใช้ JWT และโทเค็นการรีเฟรช
รับ /ภาพยนตร์/ ส่งคืนรายการภาพยนตร์เป็นออบเจ็กต์ JSON โดยต้องใช้ JWT ที่ถูกต้องซึ่งถูกส่งผ่านในส่วนหัวด้วยรูปแบบนี้:การอนุญาต:Bearer xxx ป>
รับ /ภาพยนตร์/$ID กลับรายละเอียดของภาพยนตร์ด้วยรหัส $ID
โพสต์ /auth/รีเฟรช สร้างและส่งคืน JWT ใหม่ ควรส่งโทเค็นการรีเฟรชเป็น 51 พารามิเตอร์
โค้ดสำหรับเส้นทาง API
เราเริ่มต้นด้วยจุดสิ้นสุดการลงชื่อเข้าใช้ เรามาสร้างไฟล์ 66 กัน ดังต่อไปนี้:
import bcrypt from "bcrypt";
import {
addToList,
generateAccessToken,
generateRefreshToken,
redis,
} from "../../../utils";
export default async (req, res) => {
if (req.method === "GET") {
res.status(405).send("Not Allowed");
} else {
console.log(req.body.user);
try {
const user = await redis.hgetall(`user:${req.body.user}`);
if (user) {
const validPassword = bcrypt.compare(req.body.password, user.password);
if (validPassword) {
const token = generateAccessToken(req.body.user, user.level);
const refreshToken = generateRefreshToken(req.body.user, user.level);
const refresh = await addToList(req.body.user, refreshToken);
const content = {
user: req.body.user,
level: user.level,
};
res.status(200).json({
message: "Logged in",
content: content,
JWT: token,
refresh: refreshToken,
});
} else {
res.status(400).json({ error: "Invalid Password" });
}
} else {
res.status(401).json({ error: "User not found" });
}
} catch (error) {
res.status(500).send("Internal Server Error");
}
}
}; จุดสิ้นสุดการลงชื่อเข้าใช้ของเราจะยอมรับ POST ที่มีพารามิเตอร์สองตัวเท่านั้น:ผู้ใช้ และรหัสผ่าน . ก่อนอื่น เราจะตรวจสอบว่ามีผู้ใช้อยู่ในฐานข้อมูล Redis ของเราด้วย:
const user = await redis.hgetall(`user:${req.body.user}`);
หากมีผู้ใช้อยู่ เราจะเปรียบเทียบรหัสผ่านที่เข้ารหัส:
const validPassword = bcrypt.compare(req.body.password, user.password);
ณ จุดนี้ หากรหัสผ่านตรงกัน เราสามารถถือว่าผู้ใช้ได้รับการรับรองความถูกต้อง และเราสามารถส่ง JWT และโทเค็นการรีเฟรชกลับมาได้ เรายังจัดเก็บโทเค็นการรีเฟรชไว้ในอินสแตนซ์ Redis ของเรา ในการทำเช่นนั้น เราใช้ฟังก์ชันบางอย่างที่อยู่ในไฟล์ภายนอกที่เรียกว่า 74 ป>
ถือเป็นความรับผิดชอบของลูกค้าในการจัดเก็บโทเค็นที่ส่งคืน ใช้โทเค็นเพื่ออนุญาตเมื่อจำเป็น และรีเฟรชเมื่อหมดอายุ
เรามีฟังก์ชั่นในการสร้าง Token ของเรา 83 หนึ่งรายการเพื่อสร้างโทเค็นการรีเฟรชของเรา 92 และอีกอันสำหรับเก็บโทเค็นการรีเฟรชใน Redis 105 ของเรา . 115รหัส> นี้ ไฟล์นี้ยังใช้เพื่อเก็บฟังก์ชันยูทิลิตีและการอ้างอิงอื่นๆ ทั้งหมด (เช่น การเชื่อมต่อ Redis การตรวจสอบโทเค็นและรีเฟรช และอื่นๆ):
import { Redis } from "@upstash/redis";
import jwt from "jsonwebtoken";
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
export function generateAccessToken(username, email, level) {
return jwt.sign(
{ user: username, email: email, level: level },
process.env.SECRET_TOKEN,
{
expiresIn: "1h",
},
);
}
export function generateRefreshToken(username, email, level) {
return jwt.sign(
{ user: username, email: email, level: level },
process.env.SECRET_RTOKEN,
{
expiresIn: "30d",
},
);
}
export async function addToList(user, refresher) {
try {
await redis.hset("refresh:" + user, { refresh: refresher });
} catch (error) {
console.log(error);
}
}
export async function tokenRefresh(refreshtoken, res) {
var decoded = "";
try {
decoded = jwt.verify(refreshtoken, process.env.SECRET_RTOKEN);
} catch (error) {
return res.status(401).send("Can't refresh. Invalid Token");
}
if (decoded) {
try {
const rtoken = await redis.hget("refresh:" + decoded.user, "refresh");
console.log(rtoken);
if (rtoken !== refreshtoken) {
return res.status(401).send("Can't refresh. Invalid Token");
} else {
const user = await redis.hgetall(`user:${decoded.user}`);
console.log(user);
const token = generateAccessToken(decoded.user, user.level);
const refreshToken = generateRefreshToken(decoded.user, user.level);
const refresh = await addToList(decoded.user, refreshToken);
const content = {
user: decoded.user,
level: user.level,
};
return {
message: "Token Refreshed",
content: content,
JWT: token,
refresh: refreshToken,
};
}
} catch (error) {
console.log(error);
}
}
}
export async function verifyToken(token, res) {
try {
const decoded = jwt.verify(token, process.env.SECRET_TOKEN);
return decoded;
} catch (err) {
return res.status(405).send("Token is invalid");
}
}
ตอนนี้เราสามารถใช้เครื่องมือ (เช่นบุรุษไปรษณีย์) เพื่อทดสอบกระบวนการลงนามได้โดยการโพสต์ไปที่ 129 และส่งชื่อผู้ใช้ (me@home.org ) และรหัสผ่าน (รหัสผ่าน ) คุณควรส่งคืนออบเจ็กต์ JSON ที่มีรายละเอียดผู้ใช้ พร้อมด้วย JWT และโทเค็นการรีเฟรช:
หากทุกอย่างถูกต้อง ในฐานข้อมูล Redis ของคุณ ตอนนี้คุณควรเห็นรายการแฮชใหม่สำหรับโทเค็นการรีเฟรชที่สร้างขึ้นใหม่:
ต่อไป เรากำลังดำเนินการขั้นตอนการรับรองความถูกต้องให้เสร็จสิ้น โดยการเข้ารหัสเส้นทางการรีเฟรชโทเค็น 133 ป>
import { redis, tokenRefresh } from "../../../utils";
export default async (req, res) => {
if (req.method === "GET") {
res.status(405).send("Not Allowed");
} else {
console.log(req.body.refresh);
const refresp = await tokenRefresh(req.body.refresh, res);
res.status(200).json(refresp);
}
};
มันใช้ tokenRefresh ฟังก์ชั่นจาก 148 เริ่มต้นด้วยการตรวจสอบว่าโทเค็นนั้นถูกต้องและสามารถถอดรหัสได้ จากนั้นจะตรวจสอบ Redis ว่าผู้ใช้ได้รับโทเค็นการรีเฟรชหรือไม่ (อันที่เราเก็บไว้ก่อนหน้านี้ด้วย addToList ) หากทุกอย่างถูกต้องจะสร้าง JWT ใหม่ โทเค็นการรีเฟรชใหม่ (และจัดเก็บอีกครั้งใน Redis) และส่งทุกอย่างกลับไปยังไคลเอนต์
เราสามารถทดสอบตำแหน่งข้อมูลนี้โดยใช้เครื่องมือของเรา โดยโพสต์ไปที่ 159 และส่งโทเค็นการรีเฟรชเป็นพารามิเตอร์:
ตอนนี้ ไคลเอนต์สมมติของเราสามารถเข้าสู่ระบบและรีเฟรชโทเค็นได้ มาดูกันว่าโทเค็นสามารถใช้เพื่อส่งคำขอที่มีการตรวจสอบสิทธิ์ได้อย่างไร
สร้างเส้นทาง API ใหม่:168 ที่จะใช้ในการรับรายชื่อภาพยนตร์และรับรายละเอียดภาพยนตร์:
import { redis, verifyToken } from "../../../utils";
export default async (req, res) => {
var id;
console.log(req.query);
if (req.query.id) {
id = req.query.id[0];
}
var decoded = "";
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
return res.status(403).send("A token is required for authentication");
} else {
decoded = await verifyToken(token, res);
}
if (decoded) {
if (id) {
try {
const result = await redis.hgetall(id);
console.log(result);
return res.status(200).json(result);
} catch (error) {
return res.status(500).send("Internal Server Error");
}
} else {
try {
const result = await redis.scan(0, { match: "movie:*" });
return res.status(200).json(result);
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
}
};
การใช้ verifyToken ฟังก์ชั่นจาก 171 ของเรา เราสามารถจำกัดการเข้าถึงตำแหน่งข้อมูล API ของเราได้เฉพาะผู้ใช้ที่ให้โทเค็นที่ถูกต้องเท่านั้น เราทำแบบสอบถามตัวอย่างสองสามข้อ โดยข้อแรกเพื่อรับรายชื่อภาพยนตร์
const result = await redis.scan(0, { match: ‘movie:*’ });
และรายการที่สองเพื่อรับรายละเอียดของภาพยนตร์เรื่องเดียว โดยอิงตามพารามิเตอร์รหัสในคำขอ URL:
const result = await redis.hgetall(id);
คำขอทั้งสองขึ้นอยู่กับสถานะผู้ใช้ที่ตรวจสอบผ่าน verifyToken แต่คุณสามารถผสมและจับคู่ได้ เช่น รายการสามารถเปิดเผยต่อสาธารณะ และสามารถป้องกันรายละเอียดได้ เนื่องจากเรามีระดับที่เก็บไว้ในผู้ใช้ (และในโทเค็น) เราจึงสามารถมีระดับการอนุญาตได้มากขึ้น ลองรับรายชื่อภาพยนตร์:
และรายละเอียดภาพยนตร์เรื่องเดียว:
มุมมองของลูกค้า
ดังที่เราได้กล่าวไว้ก่อนหน้านี้ เราเพียงมุ่งเน้นไปที่ส่วนของเซิร์ฟเวอร์ นี่คือขอบเขตหลักของ API ซึ่งควรเป็นนามธรรม ไม่ใช่เว็บไซต์ วิธีที่ไคลเอ็นต์ร้องขอข้อมูล (ภาษาการเขียนโปรแกรม ไลบรารี และอื่นๆ) เป็นทางเลือกของนักพัฒนาไคลเอ็นต์ เราจะจัดเตรียมรายการตำแหน่งข้อมูล สิ่งที่ปลายทางของเราคาดหวัง และสิ่งที่ปลายทางของเราส่งคืนไปยังไคลเอ็นต์ การจัดการข้อมูล ความล่าช้าในการรีเฟรช และอื่นๆ ทั้งหมดเป็นกลยุทธ์ของลูกค้า
อะไรต่อไป
นี่เป็นเพียงตัวอย่างพื้นฐานของเวิร์กโฟลว์ของ API ที่ได้รับการป้องกัน จากที่นี่ สิ่งที่สามารถปรับปรุงได้เท่านั้น:เพิ่มประสิทธิภาพวิธีการจัดเก็บข้อมูลบน Redis, ปรับปรุงความปลอดภัยในการเข้าสู่ระบบโดยจัดเก็บข้อมูลผู้ใช้บนอินสแตนซ์ Redis อื่น, ตรวจสอบและตรวจสอบความถูกต้องของข้อมูลที่ผู้ใช้ส่งก่อนหน้านี้, เพิ่มจุดสิ้นสุดเพิ่มเติม, การส่งคืนข้อมูลในรูปแบบ GraphQL, สร้างไคลเอนต์สำหรับ API ของคุณ, จำกัดการเข้าถึงจำนวนการโทรสูงสุดต่อชั่วโมง, ใช้ระดับเพื่อจำกัดการเข้าถึง… ส่วนขยายและการปรับปรุงไม่มีที่สิ้นสุด!