در این مقاله قصد داریم تا با مفهوم احراز هویت (authentication) آشنا شویم و نحوه افزودن آن به برنامه Next.js، با کمک NextAuth.js را با هم بررسی کنیم.
امروزه احراز هویت بخش کلیدی بسیاری از اپلیکیشنهای وب است. به زبان ساده، کارکرد احراز هویت به این صورت است که بررسی میکند آیا کاربر همان کسی است که میگوید یا خیر.
یک وب سایت امن اغلب از راههای متعددی برای بررسی هویت کاربر استفاده مینماید. به عنوان مثال، پس از وارد کردن نام کاربری و رمز عبور، سایت ممکن است یک کد تأیید به دستگاه ما ارسال کند، یا این که از یک برنامه خارجی مانند Google Authenticator استفاده نماید. این احراز هویت دو مرحلهای (۲FA) به افزایش امنیت کمک میکند. در این صورت، حتی اگر شخصی رمز عبور ما را یاد بگیرد، نمیتواند بدون داشتن رمز منحصر به فرد ما به حساب کاربریمان دسترسی داشته باشد.
در توسعه وب، authentication و authorization نقشهای مختلفی را ایفا میکنند:
بنابراین، authentication بررسی میکند که ما چه کسی هستیم، و authorization تعیین میکند که چه کاری میتوانیم انجام دهیم یا به چه بخشهایی دسترسی داشته باشیم.
برای این کار یک مسیر جدید در برنامه خود به نام /login
ایجاد میکنیم و کد زیر را در آن مینویسیم:
import AcmeLogo from '@/app/ui/acme-logo'; import LoginForm from '@/app/ui/login-form'; export default function LoginPage() { return ( <main className="flex items-center justify-center md:h-screen"> <div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32"> <div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36"> <div className="w-32 text-white md:w-36"> <AcmeLogo /> </div> </div> <LoginForm /> </div> </main> ); }
ما در این مقاله برای افزودن مبحث احراز هویت به برنامه Next.js از NextAuth.js استفاده خواهیم کرد. NextAuth.js بسیاری از پیچیدگیهای مربوط به مدیریت sessionها، sign-in و sign-out و سایر جنبههای احراز هویت را از بین میبرد. ما میتوانیم این ویژگیها را به صورت دستی هم پیادهسازی کنیم، اما این فرآیند میتواند زمانبر و مستعد خطا باشد. بنابراین NextAuth.js این فرآیند را سادهتر کرده و راه حلی یکپارچه برای احراز هویت در برنامههای Next.js ارائه میدهد.
NextAuth.js را با اجرای دستور زیر در ترمینال خود نصب میکنیم:
npm install next-auth@beta
در اینجا، ما نسخه بتا NextAuth.js را نصب میکنیم که با Next.js 14 سازگار است.
در مرحله بعد، یک secret key برای برنامه خود میسازیم. این کلید برای رمزگذاری کوکیها استفاده میشود و امنیت sessionهای کاربر را تضمین میکند. برای انجام این کار میتوانیم دستور زیر را در ترمینال اجرا کنیم:
openssl rand -base64 32
سپس در فایل .env
خود، کلید تولید شده را به متغیر AUTH_SECRET
اضافه مینماییم:
AUTH_SECRET=your-secret-key
برای اینکه auth در پروژه نهایی که میسازیم کار کند، باید متغیرهای محیطی خود را در پروژه Vercel خود نیز به روز رسانی کنیم. مطالعه این راهنما در مورد نحوه اضافه کردن متغیرهای محیطی در Vercel میتواند مفید باشد.
یک فایل auth.config.ts
در root پروژه خود ایجاد میکنیم که یک آبجکت authConfig
را export میکند. این آبجکت شامل گزینههای پیکربندی NextAuth.js خواهد بود. در حال حاضر، فقط گزینه pages را داریم:
import type { NextAuthConfig } from 'next-auth'; export const authConfig = { pages: { signIn: '/login', }, };
می توانیم از گزینه pages
برای تعیین مسیر سفارشی sign-in، sign-out و صفحات خطا استفاده نماییم. این مورد ضروری نیست، اما با افزودن گزینه signIn: '/login'
به pages
، کاربر به جای صفحه پیشفرض NextAuth.js به صفحه لاگین سفارشی مدنظر ما هست هدایت میشود.
در مرحله بعد، منطق محافظت از مسیرهای خود را اضافه میکنیم. این کار باعث میشود تا کاربران به صفحات داشبورد دسترسی نداشته باشند مگر اینکه وارد سیستم شوند.
import type { NextAuthConfig } from 'next-auth'; export const authConfig = { pages: { signIn: '/login', }, callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); if (isOnDashboard) { if (isLoggedIn) return true; return false; // Redirect unauthenticated users to login page } else if (isLoggedIn) { return Response.redirect(new URL('/dashboard', nextUrl)); } return true; }, }, providers: [], // Add providers with an empty array for now } satisfies NextAuthConfig;
callback authorized
مورد استفاده قرار میگیرد تا اینکه بررسی کند که آیا درخواستی که برای دسترسی به صفحه از طریق Next.js Middleware اعلام شده است مجاز است یا خیر. این تابع قبل از تکمیل درخواست فراخوانی میشود، و یک آبجکت با ویژگیهای auth
و request
دریافت میکند. ویژگی auth
شامل session کاربر و ویژگی request
شامل درخواست ورودی است.
گزینه providers
آرایهای است که در آن گزینههای مختلف برای لاگین را لیست میکنیم. در حال حاضر، این یک آرایه خالی است تا پیکربندی NextAuth را تکمیل کند. مطالعه Adding the Credentials provider میتواند مفید باشد.
پس از آن، باید آبجکت authConfig
را در یک فایل Middleware وارد کنیم. در root پروژه خود یک فایل به نام Middleware.ts
ایجاد کرده و کد زیر را در آن مینویسیم:
import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; export default NextAuth(authConfig).auth; export const config = { // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], };
در اینجا ما NextAuth.js را با آبجکت authConfig
مقداردهی اولیه کرده و ویژگی auth
را export میکنیم. همچنین برای این که مشخص کنیم که باید در مسیرهای خاصی اجرا شود، از گزینه matcher
از Middleware استفاده میکنیم.
مزیت استفاده از Middleware برای این کار این است که تا زمانی که Middleware احراز هویت را تأیید نکند، مسیرهای محافظت شده حتی شروع به رندر نمیکنند. در نتیجه امنیت و عملکرد برنامه ما افزایش پیدا میکند.
hash کردن رمزهای عبور قبل از ذخیره آنها در پایگاه داده، کار بسیار مفیدی است. hash کردن، رمز عبور را به یک رشته کاراکتر با طول ثابت تبدیل میکند که بهصورت تصادفی ظاهر میشود و حتی اگر دادههای کاربر در معرض دید دیگران قرار گیرد، لایهای از امنیت را فراهم میکند.
برای این کار ما باید یک فایل جداگانه برای پکیج bcrypt
ایجاد کنیم. دلیل این کار این است که bcrypt
به APIهای Node.js متکی است که در Next.js Middleware موجود نیستند.
یک فایل جدید به نام auth.ts
ایجاد میکنیم که آبجکت authConfig
را گسترش میدهد:
import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; export const { auth, signIn, signOut } = NextAuth({ ...authConfig, });
در مرحله بعد، باید گزینه providers
را برای NextAuth.js اضافه نماییم. providers
آرایهای است که در آن گزینههای مختلف لاگین مانند Google یا GitHub را لیست میکنیم. برای این مقاله، ما فقط بر استفاده از Credentials provider تمرکز خواهیم کرد.
Credentials provider به کاربران اجازه میدهد تا با نام کاربری و رمز عبور وارد شوند.
import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; import Credentials from 'next-auth/providers/credentials'; export const { auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [Credentials({})], });
بهتر است به این موضوع توجه داشته باشیم که، اگرچه ما از Credentials provider استفاده میکنیم، اما به طور کلی توصیه میشود از providerهای جایگزین مانند OAuth یا providerهای email استفاده نماییم. برای داشتن لیست کامل گزینهها، مطالعه مستندات NextAuth.js میتواند مفید باشد.
میتوانیم از تابع autorize
برای مدیریت منطق احراز هویت استفاده کنیم. مشابه Server Actionها، میتوانیم از zod
برای اعتبارسنجی ایمیل و رمز عبور، قبل از بررسی وجود کاربر در پایگاه داده استفاده نماییم:
import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; import Credentials from 'next-auth/providers/credentials'; import { z } from 'zod'; export const { auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [ Credentials({ async authorize(credentials) { const parsedCredentials = z .object({ email: z.string().email(), password: z.string().min(6) }) .safeParse(credentials); }, }), ], });
پس از تأیید اعتبار، یک تابع getUser
جدید ایجاد میکنیم که کاربر را از پایگاه داده درخواست میکند.
import NextAuth from 'next-auth'; import Credentials from 'next-auth/providers/credentials'; import { authConfig } from './auth.config'; import { z } from 'zod'; import { sql } from '@vercel/postgres'; import type { User } from '@/app/lib/definitions'; import bcrypt from 'bcrypt'; async function getUser(email: string): Promise<User | undefined> { try { const user = await sql<User>`SELECT * FROM users WHERE email=${email}`; return user.rows[0]; } catch (error) { console.error('Failed to fetch user:', error); throw new Error('Failed to fetch user.'); } } export const { auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [ Credentials({ async authorize(credentials) { const parsedCredentials = z .object({ email: z.string().email(), password: z.string().min(6) }) .safeParse(credentials); if (parsedCredentials.success) { const { email, password } = parsedCredentials.data; const user = await getUser(email); if (!user) return null; } return null; }, }), ], });
سپس، bcrypt.compare
را فراخوانی میکنیم تا بررسی کنیم که آیا رمزهای عبور باهم مطابقت دارند یا نه:
import NextAuth from 'next-auth'; import Credentials from 'next-auth/providers/credentials'; import { authConfig } from './auth.config'; import { sql } from '@vercel/postgres'; import { z } from 'zod'; import type { User } from '@/app/lib/definitions'; import bcrypt from 'bcrypt'; // ... export const { auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [ Credentials({ async authorize(credentials) { // ... if (parsedCredentials.success) { const { email, password } = parsedCredentials.data; const user = await getUser(email); if (!user) return null; const passwordsMatch = await bcrypt.compare(password, user.password); if (passwordsMatch) return user; } console.log('Invalid credentials'); return null; }, }), ], });
در نهایت، اگر رمزهای عبور مطابقت داشته باشند میخواهیم تا کاربر را return کنیم، در غیر این صورت، برای جلوگیری از ورود کاربر، null
را return میکنیم.
اکنون باید منطق تأیید اعتبار را به فرم لاگین خود متصل کنیم. در فایل actions.ts
خود یک action جدید به نام authenticate
ایجاد میکنیم. این action باید تابع signIn
را از auth.ts
import کند:
import { signIn } from '@/auth'; import { AuthError } from 'next-auth'; // ... export async function authenticate( prevState: string | undefined, formData: FormData, ) { try { await signIn('credentials', formData); } catch (error) { if (error instanceof AuthError) { switch (error.type) { case 'CredentialsSignin': return 'Invalid credentials.'; default: return 'Something went wrong.'; } } throw error; } }
اگر خطای 'CredentialsSignin'
وجود داشته باشد، میخواهیم یک پیام خطای مناسب نشان دهیم. برای آشنایی بیشتر در مورد خطاهای NextAuth.js مطالعه مستندات آن میتواند مفید باشد.
در نهایت، در کامپوننت login-form.tsx
، میتوانیم از useFormState
React برای فراخوانی server action و رسیدگی به خطاهای فرم، و از useFormStatus
برای رسیدگی به pending state فرم استفاده نماییم:
'use client'; import { lusitana } from '@/app/ui/fonts'; import { AtSymbolIcon, KeyIcon, ExclamationCircleIcon, } from '@heroicons/react/24/outline'; import { ArrowRightIcon } from '@heroicons/react/20/solid'; import { Button } from '@/app/ui/button'; import { useFormState, useFormStatus } from 'react-dom'; import { authenticate } from '@/app/lib/actions'; export default function LoginForm() { const [errorMessage, dispatch] = useFormState(authenticate, undefined); return ( <form action={dispatch} className="space-y-3"> <div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8"> <h1 className={`${lusitana.className} mb-3 text-2xl`}> Please log in to continue. </h1> <div className="w-full"> <div> <label className="mb-3 mt-5 block text-xs font-medium text-gray-900" htmlFor="email" > Email </label> <div className="relative"> <input className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500" id="email" type="email" name="email" placeholder="Enter your email address" required /> <AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" /> </div> </div> <div className="mt-4"> <label className="mb-3 mt-5 block text-xs font-medium text-gray-900" htmlFor="password" > Password </label> <div className="relative"> <input className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500" id="password" type="password" name="password" placeholder="Enter password" required minLength={6} /> <KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" /> </div> </div> </div> <LoginButton /> <div className="flex h-8 items-end space-x-1" aria-live="polite" aria-atomic="true" > {errorMessage && ( <> <ExclamationCircleIcon className="h-5 w-5 text-red-500" /> <p className="text-sm text-red-500">{errorMessage}</p> </> )} </div> </div> </form> ); } function LoginButton() { const { pending } = useFormStatus(); return ( <Button className="mt-4 w-full" aria-disabled={pending}> Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" /> </Button> ); }
برای افزودن logout به <SideNav />
، تابع signOut
را از auth.ts
در المنت <form>
خود فراخوانی میکنیم:
import Link from 'next/link'; import NavLinks from '@/app/ui/dashboard/nav-links'; import AcmeLogo from '@/app/ui/acme-logo'; import { PowerIcon } from '@heroicons/react/24/outline'; import { signOut } from '@/auth'; export default function SideNav() { return ( <div className="flex h-full flex-col px-3 py-4 md:px-2"> // ... <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2"> <NavLinks /> <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div> <form action={async () => { 'use server'; await signOut(); }} > <button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"> <PowerIcon className="w-6" /> <div className="hidden md:block">Sign Out</div> </button> </form> </div> </div> ); }
در این مقاله سعی کردیم تا با استفاده از NextAuth.js، مفهوم احراز هویت را به برنامه Next.js اضافه کنیم. در نهایت باید بتوانیم با استفاده از اطلاعات زیر وارد برنامه خود شده و از آن خارج شویم:
ایمیل: user@nextmail.com
رمز عبور: ۱۲۳۴۵۶
۵۰ درصد تخفیف ویژه زمستان فرانت کست تا ۱۴ دی
کد تخفیف: wnt