از طریق اجرای احراز هویت سفارشی مبتنی بر JWT، کنترل بیشتری بر منطق احراز هویت برنامه Next.js خود داشته باشید.
احراز هویت رمزی یک استراتژی محبوب است که برای محافظت از برنامه های وب و تلفن همراه از دسترسی غیرمجاز استفاده می شود. در Next.js، می توانید از ویژگی های احراز هویت ارائه شده توسط Next-auth استفاده کنید.
همچنین، میتوانید با استفاده از JSON Web Tokens (JWT) یک سیستم احراز هویت مبتنی بر توکن سفارشی ایجاد کنید. با انجام این کار، اطمینان حاصل می کنید که کنترل بیشتری بر منطق احراز هویت دارید. اساساً، سفارشی کردن سیستم برای مطابقت دقیق با نیازهای پروژه شما.
یک پروژه Next.js راه اندازی کنید
برای شروع، Next.js را با اجرای دستور زیر در ترمینال خود نصب کنید.
npx create-next-app@latest next-auth-jwt --experimental-app
این راهنما از Next.js 13 استفاده می کند که شامل فهرست برنامه است.
سپس، این وابستگی ها را با استفاده از npm، Node Package Manager، در پروژه خود نصب کنید.
npm install jose universal-cookie
Jose یک ماژول جاوا اسکریپت است که مجموعهای از ابزارها را برای کار با JSON Web Tokens ارائه میکند، در حالی که وابستگی جهانی به کوکی راه سادهای برای کار با کوکیهای مرورگر در هر دو محیط سمت سرویس گیرنده و سمت سرور ارائه میدهد.
می توانید کد این پروژه را در این مخزن GitHub پیدا کنید.
رابط کاربری فرم ورود را ایجاد کنید
دایرکتوری src/app را باز کنید، یک پوشه جدید ایجاد کنید و نام آن را Login بگذارید. در داخل این پوشه، یک فایل page.js جدید اضافه کنید و کد زیر را وارد کنید.
"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>
);
}
کد بالا یک مؤلفه کاربردی صفحه ورود ایجاد می کند که یک فرم ورود ساده را در مرورگر ارائه می کند تا به کاربران اجازه دهد نام کاربری و رمز عبور را وارد کنند.
عبارت use client در کد تضمین می کند که مرزی بین کد فقط سرور و فقط مشتری در فهرست برنامه اعلام شده است.
در این مورد، برای اعلام اینکه کد موجود در صفحه ورود به سیستم، به ویژه تابع handleSubmit فقط روی کلاینت اجرا می شود، استفاده می شود. در غیر این صورت، Next.js یک خطا ایجاد می کند.
حالا بیایید کد تابع handleSubmit را تعریف کنیم. داخل کامپوننت فانکشنال کد زیر را اضافه کنید.
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");
}
};
برای مدیریت منطق احراز هویت ورود، این تابع اعتبار کاربر را از فرم ورود می گیرد. سپس یک درخواست POST را به یک نقطه پایانی API ارسال می کند که در امتداد جزئیات کاربر برای تأیید ارسال می شود.
اگر اعتبارنامه ها معتبر باشند، نشان می دهد که فرآیند ورود با موفقیت انجام شده است – API یک وضعیت موفقیت آمیز را در پاسخ برمی گرداند. سپس تابع handler از روتر Next.js برای هدایت کاربر به یک URL مشخص، در این مورد، مسیر محافظت شده استفاده می کند.
Login API Endpoint را تعریف کنید
در پوشه src/app، یک پوشه جدید ایجاد کنید و نام آن را api بگذارید. در داخل این پوشه، یک فایل login/route.js جدید اضافه کنید و کد زیر را وارد کنید.
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: "/",
});
return response;
}
return NextResponse.json({ success: false });
}
وظیفه اصلی این API تأیید اعتبار ورود به سیستم ارسال شده در درخواستهای POST با استفاده از دادههای ساختگی است.
پس از تأیید موفقیت آمیز، یک توکن JWT رمزگذاری شده مرتبط با جزئیات تأیید شده کاربر ایجاد می کند. در نهایت، یک پاسخ موفقیت آمیز، از جمله رمز موجود در کوکی های پاسخ، را برای مشتری ارسال می کند. در غیر این صورت، پاسخ وضعیت خرابی را برمی گرداند.
منطق تأیید رمز را پیاده سازی کنید
مرحله اولیه در احراز هویت توکن، تولید رمز پس از یک فرآیند ورود موفقیت آمیز است. گام بعدی پیاده سازی منطق برای تایید توکن است.
در اصل، شما از تابع jwtVerify ارائه شده توسط ماژول Jose برای تأیید توکن های JWT که با درخواست های HTTP بعدی ارسال می شوند، استفاده خواهید کرد.
در پوشه src، یک فایل libs/auth.js جدید ایجاد کنید و کد زیر را وارد کنید.
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;
}
}
کلید مخفی در امضا و تأیید توکن ها استفاده می شود. با مقایسه امضای رمزگشایی شده با امضای مورد انتظار، سرور می تواند به طور موثر تأیید کند که نشانه ارائه شده معتبر است و در نهایت، درخواست های کاربران را تأیید کند.
فایل .env را در دایرکتوری ریشه ایجاد کنید و یک کلید مخفی منحصر به فرد به شرح زیر اضافه کنید:
NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key
یک مسیر محافظت شده ایجاد کنید
اکنون باید مسیری ایجاد کنید که فقط کاربران احراز هویت شده بتوانند به آن دسترسی داشته باشند. برای انجام این کار، یک فایل protected/page.js جدید در دایرکتوری src/app ایجاد کنید. داخل این فایل کد زیر را اضافه کنید.
export default function ProtectedPage() {
return <h1>Very protected page</h1>;
}
یک هوک برای مدیریت وضعیت احراز هویت ایجاد کنید
یک پوشه جدید در پوشه src ایجاد کنید و نام آن را hook بگذارید. داخل این پوشه یک فایل useAuth/index.js جدید اضافه کنید و کد زیر را وارد کنید.
"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;
}
این قلاب وضعیت احراز هویت را در سمت مشتری مدیریت می کند. اعتبار نشانه JWT موجود در کوکی ها را با استفاده از تابع verifyJwtToken واکشی و تأیید می کند، و سپس جزئیات کاربر احراز هویت شده را روی وضعیت احراز هویت تنظیم می کند.
با انجام این کار، به اجزای دیگر اجازه می دهد تا به اطلاعات کاربر تأیید شده دسترسی داشته باشند و از آنها استفاده کنند. این برای سناریوهایی مانند ایجاد بهروزرسانیهای رابط کاربری بر اساس وضعیت احراز هویت، درخواستهای بعدی API یا ارائه محتوای مختلف بر اساس نقشهای کاربر ضروری است.
در این مورد، از هوک برای ارائه محتوای مختلف در مسیر اصلی بر اساس وضعیت احراز هویت یک کاربر استفاده خواهید کرد.
یک رویکرد جایگزین که ممکن است در نظر بگیرید، مدیریت دولتی با استفاده از Redux Toolkit یا استفاده از ابزار مدیریت دولتی مانند Jotai است. این رویکرد تضمین می کند که اجزا می توانند به وضعیت احراز هویت یا هر حالت تعریف شده دیگری دسترسی جهانی داشته باشند.
ادامه دهید و فایل app/page.js را باز کنید، کد Next.js boilerplate را حذف کنید و کد زیر را اضافه کنید.
"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="/login">Login</Link>
)}
</nav>
</header>
</>
}
کد بالا از قلاب useAuth برای مدیریت وضعیت احراز هویت استفاده می کند. با انجام این کار، زمانی که کاربر احراز هویت نشده باشد، یک صفحه اصلی عمومی با پیوندی به مسیر صفحه ورود به سیستم را به صورت مشروط ارائه می کند و یک پاراگراف را برای کاربر تأیید شده نمایش می دهد.
برای اعمال دسترسی مجاز به مسیرهای محافظت شده، یک میان افزار اضافه کنید
در پوشه src، یک فایل Middleware.js جدید ایجاد کنید و کد زیر را اضافه کنید.
import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";
const AUTH_PAGES = ["/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: ["/login", "/protected/:path*"] };
این کد میانافزار به عنوان یک محافظ عمل میکند. بررسی میکند تا اطمینان حاصل شود که وقتی کاربران میخواهند به صفحات محافظت شده دسترسی داشته باشند، احراز هویت شده و مجاز به دسترسی به مسیرها هستند، علاوه بر این، کاربران غیرمجاز را به صفحه ورود هدایت میکند.
ایمن سازی برنامه های Next.js
احراز هویت توکن یک مکانیسم امنیتی موثر است. با این حال، این تنها استراتژی موجود برای محافظت از برنامه های شما در برابر دسترسی غیرمجاز نیست.
برای تقویت برنامهها در برابر چشمانداز پویای امنیت سایبری، اتخاذ یک رویکرد امنیتی جامع که بهطور جامع به حفرهها و آسیبپذیریهای امنیتی احتمالی رسیدگی میکند تا حفاظت کامل را تضمین کند، مهم است.