Trong bài viết này, chúng tôi sẽ xây dựng một dịch vụ Rest API được xác thực tối thiểu nhưng đầy đủ chức năng, tận dụng các Tuyến API Next.js và Upstash Redis mà chúng tôi sẽ sử dụng làm hệ thống lưu trữ/bộ nhớ đệm siêu nhanh cho cả dữ liệu, xác thực người dùng và xử lý JWT của chúng tôi. Xin lưu ý rằng dự án này sẽ không có giao diện người dùng mà chỉ hiển thị API có thể được truy vấn với nhiều ứng dụng khách khác nhau.
Điều kiện tiên quyết
Để làm theo hướng dẫn, bạn sẽ cần:
- Tài khoản Upstash — Đăng ký tài khoản miễn phí tại đây
- Kiến thức cơ bản về Redis
- Kiến thức cơ bản về các tuyến API Next.js
- Kiến thức cơ bản về quy trình xác thực và ủy quyền
- Một công cụ bạn chọn để thực hiện các yêu cầu HTTP
Upstash Redis là gì
Upstash là cơ sở dữ liệu đám mây trong bộ nhớ không có máy chủ dựa trên Redis. Chúng tôi sẽ sử dụng nó để lưu trữ dữ liệu sẽ được API của chúng tôi phân phát, chúng tôi cũng sẽ lưu trữ trong Upstash Redis cơ sở người dùng và mã thông báo người dùng của chúng tôi.
Chúng ta sẽ xây dựng những gì
Chúng tôi sẽ mã hóa dịch vụ API REST cho phép các ứng dụng khách yêu cầu dữ liệu từ nó (trong trường hợp cụ thể này là danh sách phim); chúng tôi sẽ bảo mật các điểm cuối bằng JWT, chúng tôi sẽ mã hóa dịch vụ đăng nhập API để nhận mã thông báo và chúng tôi cũng triển khai quy trình làm mới mã thông báo.
Chúng tôi sẽ không tập trung về phát triển khách hàng (vì chúng tôi đang xây dựng một dịch vụ 'không có ý kiến'), nhưng chúng tôi sẽ cung cấp các thông số kỹ thuật của dịch vụ của mình để bất kỳ ai cũng có thể xây dựng khách hàng cho dịch vụ đó.
Kho lưu trữ và bản trình diễn
Để làm theo, bạn có thể muốn sao chép kho dự án
Nguồn trên GitHub
Bạn cũng có thể thử bản demo tại URL sau:
https://upstash-dwov9jbiq-popland.vercel.app/api/auth/signin
Để kết nối với dịch vụ, hãy tạo tên người dùng chuyển Yêu cầu POST HTTP (me@home.org ) và mật khẩu (mật khẩu ) như trong ví dụ sau (sử dụng Postman):
Thiết lập cơ sở dữ liệu Redis
Trước hết, bạn cần đăng ký Upstash Redis (gói miễn phí sẽ hoạt động cho mục đích thử nghiệm), sau khi đăng nhập xong vào bảng điều khiển, bạn có thể tạo cơ sở dữ liệu mới:
Tiếp tục bằng cách nhấp vào “Tạo cơ sở dữ liệu”, đặt tên là MovieManager và đặt là Toàn cầu. Bây giờ, chúng tôi thêm một số dữ liệu giả bằng cách sử dụng Upstash CLI
Chúng tôi sẽ thêm một số phim dưới dạng hàm băm Redis (về cơ bản chúng là đối tượng) bằng lệnh 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
Chúng tôi cũng sẽ thêm người dùng được phép truy cập dữ liệu, người dùng cũng sẽ là hàm băm Redis:
hmset user:’me@home.org’ password $2b$10$zctxUVDyy3jzvSp68oKpMOnkyra4R.NzOFVh9aii3Y43X7XtetoyK level 0
Xin lưu ý :Mật khẩu được mã hóa bằng bcrypt (nói nôm na thì đó là mật khẩu ), thông thường, những người dùng cần quyền truy cập vào sổ đăng ký API thông qua một trang web (hoặc lấy thông tin xác thực của họ trên một trang web), trong ví dụ này, chúng tôi không cung cấp điểm cuối đăng ký
Mọi lệnh được nhập trong Upstash CLI sẽ cung cấp cho bạn câu trả lời OK nếu mọi thứ đều chính xác và bạn đi tới Trình duyệt dữ liệu và chọn Hash sẽ có danh sách dữ liệu bạn đã chèn
Quy trình ủy quyền
Như đã đề cập trước đó, điểm cuối của chúng tôi không thể truy cập công khai, vì vậy chúng tôi cần một cách để xác thực và ủy quyền cho người dùng. Để xác thực, chúng tôi cung cấp điểm cuối đăng nhập; để ủy quyền, điểm cuối được bảo vệ yêu cầu Tiêu đề ủy quyền, được gửi cùng với yêu cầu. Đây là cách quy trình làm việc chi tiết:
- Người dùng yêu cầu điểm cuối đăng nhập, đăng tên người dùng và mật khẩu
- Máy chủ cố gắng xác thực người dùng, nếu người dùng hợp lệ thì máy chủ sẽ tạo và gửi lại JWT (Mã thông báo web JSON) và Mã thông báo làm mới, Mã thông báo làm mới cũng được lưu trữ trên phiên bản Upstash Redis của chúng tôi
- Khách hàng nhận lại mã thông báo và lưu trữ chúng ở đâu đó (khách hàng có trách nhiệm về cách thức/nơi lưu trữ chúng)
- Khách hàng yêu cầu điểm cuối được bảo vệ, gửi JWT trong Tiêu đề
- Máy chủ nhận JWT, xác minh nó và nếu nó được xác minh sẽ gửi lại dữ liệu mà khách hàng yêu cầu
- Sau khi JWT hết hạn hoặc sắp hết hạn, khách hàng có thể yêu cầu JWT mới mà không cần đăng nhập lại bằng cách gửi Mã thông báo làm mới đến một điểm cuối cụ thể.
- Máy chủ nhận được Mã thông báo làm mới, xác minh nó và nếu xác minh dương tính sẽ cấp JWT và Mã thông báo làm mới mới, hãy gửi chúng trở lại máy khách và lưu trữ lại Mã thông báo làm mới mới
JWT và Refresh Token có cùng định dạng, có thông tin gần như giống nhau nhưng sử dụng 2 key khác nhau (chúng ta sẽ thiết lập trong .env của mình file) và có hai thời hạn khác nhau:một thời hạn ngắn cho JWT (vì đây là mã thông báo được sử dụng nhiều nhất trong một phiên nên chúng tôi sẽ sớm hết hạn, đề phòng trường hợp nó bị chặn) và một thời hạn dài cho Mã thông báo làm mới. Thời hạn của cả hai tùy thuộc vào mức độ an toàn mà bạn cần ở lại; thông thường, JWT sẽ hết hạn sau chưa đầy một giờ và Mã thông báo làm mới có thể tồn tại trong một tháng. Nếu cả hai mã thông báo hết hạn, người dùng cần đăng nhập lại.
Thiết lập dự án
Sau khi hoàn tất cơ sở dữ liệu Upstash Redis, chúng ta có thể bắt đầu khởi tạo dự án của mình; đầu tiên, chúng ta tạo một dự án Next.js mới:
npx create-next-app upstash-jwt
Sau đó chúng ta nhập vào thư mục mới tạo upstash-jwt và chúng tôi cài đặt các mô-đun cần thiết:
npm i bcrypt jsonwebtoken @upstash/redis
Tạo một .env.local để lưu trữ khóa của bạn và điền dữ liệu chính xác
SECRET_TOKEN=
SECRET_RTOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= Tạo SECRET_TOKEN và SECRET_RTOKEN sẽ được sử dụng để tạo JWT, hãy nhớ rằng các khóa này phải được giữ bí mật và phải rất ngẫu nhiên/khó đoán, bạn có thể sử dụng chuỗi 64 bit thành chuỗi Hex. Nhận UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN từ Bảng điều khiển Upstash, Tab Chi tiết, Phần API còn lại:
Bây giờ chúng ta có thể bắt đầu bố trí các điểm cuối của mình:
ĐĂNG /auth/đăng nhập Nó đăng nhập người dùng, cần email và mật khẩu được chuyển dưới dạng đối tượng JSON {"email":"email", "password": "password"} , nó trả về một đối tượng JSON có thông tin người dùng là JWT và Mã thông báo làm mới.
NHẬN /phim/ Trả về danh sách phim dưới dạng đối tượng JSON, nó yêu cầu JWT hợp lệ được chuyển vào tiêu đề với định dạng sau:Ủy quyền:Bearer xxx
NHẬN /phim/$ID Trả về thông tin chi tiết của phim với id $ID
ĐĂNG/xác thực/làm mới Tạo và trả về JWT mới, Mã thông báo làm mới phải được chuyển dưới dạng refreshToken tham số.
Mã cho các tuyến API
Chúng ta bắt đầu với điểm cuối đăng nhập, hãy tạo tệp pages/api/auth/signin.js như sau:
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");
}
}
}; Điểm cuối đăng nhập của chúng tôi sẽ chỉ chấp nhận POST với hai tham số:người dùng và mật khẩu . Trước hết, chúng tôi kiểm tra xem người dùng có hiện diện trong cơ sở dữ liệu Redis của chúng tôi hay không bằng:
const user = await redis.hgetall(`user:${req.body.user}`);
Nếu người dùng có mặt, chúng tôi sẽ so sánh mật khẩu được mã hóa:
const validPassword = bcrypt.compare(req.body.password, user.password);
Tại thời điểm này, nếu mật khẩu khớp, chúng tôi có thể giả sử người dùng đã được xác thực và chúng tôi có thể gửi lại JWT và Mã thông báo làm mới, chúng tôi cũng lưu trữ mã thông báo làm mới trong phiên bản Redis của mình. Để làm như vậy, chúng tôi sử dụng một số hàm có trong tệp bên ngoài có tên utils.js
Khách hàng có trách nhiệm lưu trữ các mã thông báo được trả lại, sử dụng chúng để ủy quyền khi cần và làm mới chúng khi hết hạn
Chúng tôi có chức năng tạo Token generateAccessToken , một để tạo Mã thông báo làm mới generateRefreshToken của chúng tôi và một để lưu trữ Refresh Token trong Redis addToList của chúng tôi . utils.js này tệp này cũng sẽ được sử dụng để giữ tất cả các chức năng tiện ích và tài liệu tham khảo khác (như kết nối Redis, xác minh và làm mới mã thông báo, v.v.):
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");
}
}
Bây giờ, chúng ta có thể sử dụng một công cụ (như Postman) để kiểm tra quá trình ký, bằng cách đăng lên (http://localhost:3000/api/auth/signin và chuyển tên người dùng (me@home.org ) và mật khẩu (mật khẩu ), bạn sẽ lấy lại một đối tượng JSON chứa thông tin chi tiết về người dùng cùng với JWT và Mã thông báo làm mới:
Nếu mọi thứ đều chính xác thì trong cơ sở dữ liệu Redis của bạn, bây giờ bạn sẽ thấy mục nhập Hash mới cho Mã thông báo làm mới mới được tạo:
Tiếp theo, chúng tôi đang hoàn tất quy trình xác thực bằng cách mã hóa lộ trình làm mới mã thông báo refresh.js
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);
}
};
nó sử dụng tokenRefresh chức năng từ utils.js nó bắt đầu bằng cách xác minh rằng mã thông báo hợp lệ và có thể được giải mã, sau đó nó kiểm tra trên Redis xem người dùng có nhận được mã thông báo làm mới hay không (mã chúng tôi đã lưu trữ trước đó với addToList ), nếu mọi thứ đều chính xác, nó sẽ tạo JWT mới, Mã thông báo làm mới mới (và lưu trữ lại trong Redis) và gửi mọi thứ trở lại máy khách.
Chúng tôi có thể kiểm tra điểm cuối này bằng công cụ của mình, đăng lên http://localhost:3000/api/auth/refresh và chuyển Mã thông báo làm mới dưới dạng tham số:
Bây giờ, ứng dụng khách giả định của chúng tôi có thể đăng nhập và làm mới mã thông báo của mình, hãy xem cách sử dụng mã thông báo để thực hiện các yêu cầu được xác thực.
Tạo tuyến API mới:api/movies/[[...id]].js sẽ được sử dụng để lấy danh sách phim và để biết thông tin chi tiết về phim:
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");
}
}
}
};
Sử dụng verifyToken hoạt động từ utils.js của chúng tôi chúng tôi chỉ có thể hạn chế quyền truy cập vào điểm cuối API của mình đối với những người dùng cung cấp Mã thông báo hợp lệ. Chúng tôi đã thực hiện một số truy vấn mẫu, truy vấn đầu tiên nhận được danh sách phim
const result = await redis.scan(0, { match: ‘movie:*’ });
và thứ hai để lấy thông tin chi tiết của một bộ phim, dựa trên thông số id trong yêu cầu URL:
const result = await redis.hgetall(id);
Cả hai yêu cầu đều phụ thuộc vào trạng thái người dùng được kiểm tra qua verifyToken nhưng bạn có thể trộn và kết hợp, ví dụ:danh sách có thể được công khai và các chi tiết có thể được bảo vệ. Vì chúng tôi cũng có một cấp độ được lưu trữ trong người dùng (và trong mã thông báo), nên chúng tôi có thể có nhiều cấp độ ủy quyền hơn. Hãy thử lấy danh sách các phim:
và một chi tiết phim duy nhất:
Góc nhìn khách hàng
Như chúng tôi đã nói trước đây, chúng tôi chỉ tập trung vào phần máy chủ, đây là phạm vi chính của API, nó phải trừu tượng chứ không phải là một trang web. Cách khách hàng yêu cầu dữ liệu (ngôn ngữ lập trình, thư viện, v.v.) là lựa chọn của nhà phát triển khách hàng, chúng tôi cung cấp danh sách các điểm cuối, những gì điểm cuối của chúng tôi mong đợi và những gì điểm cuối của chúng tôi trả về cho khách hàng. Tất cả việc xử lý dữ liệu, độ trễ làm mới, v.v. đều là chiến lược của khách hàng.
Tiếp theo là gì
Đây chỉ là một ví dụ cơ bản về quy trình làm việc của API được bảo vệ. Từ đây, mọi việc chỉ có thể được cải thiện:tối ưu hóa cách lưu trữ dữ liệu trên Redis, cải thiện bảo mật đăng nhập bằng cách lưu trữ dữ liệu người dùng trên một phiên bản Redis khác, kiểm tra và xác thực dữ liệu mà người dùng gửi trước đó, thêm nhiều điểm cuối hơn, trả về dữ liệu ở định dạng GraphQL, xây dựng ứng dụng khách cho API của bạn, giới hạn quyền truy cập ở số lượng cuộc gọi tối đa mỗi giờ, sử dụng các cấp độ để giới hạn quyền truy cập… tiện ích mở rộng và cải tiến là vô tận!