Cách triển khai xác thực mã thông báo trong Next.js bằng JWT

Xác thực mã thông báo là một chiến lược phổ biến được sử dụng để bảo vệ các ứng dụng web và thiết bị di động khỏi bị truy cập trái phép. Trong Next.js, bạn có thể sử dụng các tính năng xác thực do Next-auth cung cấp.
Ngoài ra, bạn có thể chọn phát triển hệ thống xác thực dựa trên mã thông báo tùy chỉnh bằng cách sử dụng Mã thông báo Web JSON (JWT). Bằng cách đó, bạn đảm bảo rằng bạn có nhiều quyền kiểm soát hơn đối với logic xác thực; về cơ bản, tùy chỉnh hệ thống để phù hợp chính xác với yêu cầu của dự án của bạn.
Mục lục
Thiết lập dự án Next.js
Để bắt đầu, hãy cài đặt Next.js bằng cách chạy lệnh bên dưới trên thiết bị đầu cuối của bạn.
npx create-next-app@latest next-auth-jwt --experimental-app
Hướng dẫn này sẽ sử dụng Next.js 13 bao gồm thư mục ứng dụng.
Tiếp theo, cài đặt các phần phụ thuộc này trong dự án của bạn bằng npm, Trình quản lý gói nút.
npm install jose universal-cookie
Jose là một mô-đun JavaScript cung cấp một bộ tiện ích để làm việc với Mã thông báo Web JSON trong khi cookie phổ quát dependency cung cấp một cách đơn giản để làm việc với cookie của trình duyệt trong cả môi trường phía máy khách và phía máy chủ.
Tạo giao diện người dùng biểu mẫu đăng nhập
Mở thư mục src/app, tạo một thư mục mới và đặt tên là login. Trong thư mục này, thêm tệp page.js mới và bao gồm mã bên dưới.
"use client";
import { useRouter } from "next/navigation";export default function LoginPage() {
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input type="text" name="username" />
</label>
<label>
Password:
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</form>
);
}
Đoạn mã trên tạo thành phần chức năng của trang đăng nhập sẽ hiển thị một biểu mẫu đăng nhập đơn giản trên trình duyệt để cho phép người dùng nhập tên người dùng và mật khẩu.
Câu lệnh use client trong mã đảm bảo rằng ranh giới được khai báo giữa mã chỉ dành cho máy chủ và mã chỉ dành cho máy khách trong thư mục ứng dụng.
Trong trường hợp này, nó được sử dụng để khai báo rằng mã trong trang đăng nhập, cụ thể, hàm handSubmit chỉ được thực thi trên máy khách; nếu không, Next.js sẽ báo lỗi.
Bây giờ, hãy xác định mã cho hàm handSubmit. Bên trong thành phần chức năng, thêm đoạn mã sau.
const router = useRouter();const handleSubmit = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const username = formData.get("username");
const password = formData.get("password");
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
const { success } = await res.json();
if (success) {
router.push("/protected");
router.refresh();
} else {
alert("Login failed");
}
};
Để quản lý logic xác thực đăng nhập, hàm này ghi lại thông tin xác thực của người dùng từ biểu mẫu đăng nhập. Sau đó, nó sẽ gửi yêu cầu POST đến điểm cuối API chuyển thông tin chi tiết về người dùng để xác minh.
Nếu thông tin đăng nhập hợp lệ, cho biết quá trình đăng nhập đã thành công—API trả về trạng thái thành công trong phản hồi. Sau đó, hàm xử lý sẽ sử dụng bộ định tuyến của Next.js để điều hướng người dùng đến một URL được chỉ định, trong trường hợp này là tuyến đường được bảo vệ.
Xác định điểm cuối API đăng nhập
Trong thư mục src/app, tạo một thư mục mới và đặt tên là api. Trong thư mục này, thêm tệp login/route.js mới và bao gồm mã bên dưới.
import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";export async function POST(request) {
const body = await request.json();
if (body.username === "admin" && body.password === "admin") {
const token = await new SignJWT({
username: body.username,
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("30s")
.sign(getJwtSecretKey());
const response = NextResponse.json(
{ success: true },
{ status: 200, headers: { "content-type": "application/json" } }
);
response.cookies.set({
name: "token",
value: token,
path: "https://www.makeuseof.com/",
});
return response;
}
return NextResponse.json({ success: false });
}
Nhiệm vụ chính của API này là xác minh thông tin đăng nhập được chuyển trong yêu cầu POST bằng dữ liệu mô phỏng.
Sau khi xác minh thành công, nó sẽ tạo mã thông báo JWT được mã hóa liên kết với chi tiết người dùng được xác thực. Cuối cùng, nó sẽ gửi phản hồi thành công tới máy khách, bao gồm mã thông báo trong cookie phản hồi; nếu không, nó sẽ trả về phản hồi trạng thái lỗi.
Triển khai logic xác minh mã thông báo
Bước đầu tiên trong xác thực mã thông báo là tạo mã thông báo sau quá trình đăng nhập thành công. Bước tiếp theo là triển khai logic để xác minh mã thông báo.
Về cơ bản, bạn sẽ sử dụng hàm jwtVerify do mô-đun Jose cung cấp để xác minh mã thông báo JWT được chuyển cùng với các yêu cầu HTTP tiếp theo.
Trong thư mục src, tạo tệp libs/auth.js mới và thêm mã bên dưới.
import { jwtVerify } from "jose";export function getJwtSecretKey() {
const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
if (!secret) {
throw new Error("JWT Secret key is not matched");
}
return new TextEncoder().encode(secret);
}export async function verifyJwtToken(token) {
try {
const { payload } = await jwtVerify(token, getJwtSecretKey());
return payload;
} catch (error) {
return null;
}
}
Khóa bí mật được sử dụng để ký và xác minh mã thông báo. Bằng cách so sánh chữ ký mã thông báo đã giải mã với chữ ký dự kiến, máy chủ có thể xác minh một cách hiệu quả rằng mã thông báo được cung cấp là hợp lệ và cuối cùng, ủy quyền cho các yêu cầu của người dùng.
Tạo tệp .env trong thư mục gốc và thêm khóa bí mật duy nhất như sau:
NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key
Tạo một tuyến đường được bảo vệ
Bây giờ, bạn cần tạo một tuyến đường mà chỉ những người dùng được xác thực mới có thể truy cập được. Để làm như vậy, hãy tạo một tệp protected/page.js mới trong thư mục src/app. Bên trong tập tin này, thêm đoạn mã sau.
export default function ProtectedPage() {
return <h1>Very protected page</h1>;
}
Tạo một hook để quản lý trạng thái xác thực
Tạo một thư mục mới trong thư mục src và đặt tên là hooks. Bên trong thư mục này, hãy thêm tệp useAuth/index.js mới và bao gồm mã bên dưới.
"use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";export function useAuth() {
const [auth, setAuth] = React.useState(null);const getVerifiedtoken = async () => {
const cookies = new Cookies();
const token = cookies.get("token") ?? null;
const verifiedToken = await verifyJwtToken(token);
setAuth(verifiedToken);
};
React.useEffect(() => {
getVerifiedtoken();
}, []);
return auth;
}
Móc này quản lý trạng thái xác thực ở phía máy khách. Nó tìm nạp và xác minh tính hợp lệ của mã thông báo JWT có trong cookie bằng cách sử dụng hàm verifyJwtToken, sau đó đặt chi tiết người dùng được xác thực thành trạng thái xác thực.
Bằng cách đó, nó cho phép các thành phần khác truy cập và sử dụng thông tin của người dùng đã được xác thực. Điều này rất cần thiết cho các tình huống như thực hiện cập nhật giao diện người dùng dựa trên trạng thái xác thực, thực hiện các yêu cầu API tiếp theo hoặc hiển thị nội dung khác nhau dựa trên vai trò của người dùng.
Trong trường hợp này, bạn sẽ sử dụng hook để hiển thị nội dung khác nhau trên tuyến đường về nhà dựa trên trạng thái xác thực của người dùng.
Một cách tiếp cận khác mà bạn có thể cân nhắc là xử lý việc quản lý trạng thái bằng Bộ công cụ Redux hoặc sử dụng công cụ quản lý trạng thái như Jotai. Cách tiếp cận này đảm bảo các thành phần có thể có quyền truy cập toàn cầu vào trạng thái xác thực hoặc bất kỳ trạng thái được xác định nào khác.
Hãy tiếp tục và mở tệp app/page.js, xóa mã Next.js soạn sẵn và thêm mã sau đây.
"use client" ;import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
const auth = useAuth();
return <>
<h1>Public Home Page</h1>
<header>
<nav>
{auth ? (
<p>logged in</p>
) : (
<Link href="https://wilku.top/login">Login</Link>
)}
</nav>
</header>
</>
}
Đoạn mã trên sử dụng hook useAuth để quản lý trạng thái xác thực. Khi làm như vậy, nó sẽ hiển thị một cách có điều kiện một trang chủ công khai có liên kết đến tuyến trang đăng nhập khi người dùng chưa được xác thực và hiển thị một đoạn văn cho người dùng đã được xác thực.
Thêm phần mềm trung gian để thực thi quyền truy cập được ủy quyền vào các tuyến đường được bảo vệ
Trong thư mục src, tạo tệp middleware.js mới và thêm mã bên dưới.
import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";const AUTH_PAGES = ["https://wilku.top/login"];
const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));
export async function middleware(request) {
const { url, nextUrl, cookies } = request;
const { value: token } = cookies.get("token") ?? { value: null };
const hasVerifiedToken = token && (await verifyJwtToken(token));
const isAuthPageRequested = isAuthPages(nextUrl.pathname);if (isAuthPageRequested) {
if (!hasVerifiedToken) {
const response = NextResponse.next();
response.cookies.delete("token");
return response;
}
const response = NextResponse.redirect(new URL(`/`, url));
return response;
}if (!hasVerifiedToken) {
const searchParams = new URLSearchParams(nextUrl.searchParams);
searchParams.set("next", nextUrl.pathname);
const response = NextResponse.redirect(
new URL(`/login?${searchParams}`, url)
);
response.cookies.delete("token");
return response;
}return NextResponse.next();
}
export const config = { matcher: ["https://wilku.top/login", "/protected/:path*"] };
Mã phần mềm trung gian này hoạt động như một người bảo vệ. Nó kiểm tra để đảm bảo rằng khi người dùng muốn truy cập các trang được bảo vệ, họ được xác thực và được phép truy cập các tuyến đường, ngoài ra còn chuyển hướng người dùng trái phép đến trang đăng nhập.
Bảo mật các ứng dụng Next.js
Xác thực mã thông báo là một cơ chế bảo mật hiệu quả. Tuy nhiên, đây không phải là chiến lược duy nhất hiện có để bảo vệ ứng dụng của bạn khỏi bị truy cập trái phép.
Để củng cố các ứng dụng trước bối cảnh an ninh mạng năng động, điều quan trọng là phải áp dụng phương pháp bảo mật toàn diện nhằm giải quyết một cách tổng thể các lỗ hổng và lỗ hổng bảo mật tiềm ẩn để đảm bảo khả năng bảo vệ triệt để.